feat(readme): enhance README with versioning, licensing, and system dependencies
feat(profile): implement profile announcement and publishing on peer discovery fix(directory): refresh known peers on modal open for immediate visibility
This commit is contained in:
parent
ed7e6d6239
commit
1940bb3fbf
167
README.md
167
README.md
|
|
@ -1,6 +1,20 @@
|
|||
# dusk chat
|
||||
<div align="center">
|
||||
<img src="src-tauri/icons/128x128.png" alt="dusk chat" width="100" />
|
||||
|
||||
a peer-to-peer community chat platform. no central server. your data stays yours.
|
||||
<h1>dusk chat</h1>
|
||||
<p>a peer-to-peer community chat platform. no central server. your data stays yours.</p>
|
||||
|
||||
<p>
|
||||
<img alt="version" src="https://img.shields.io/badge/version-0.1.0-FF4F00?style=flat-square&labelColor=000000" />
|
||||
<img alt="license" src="https://img.shields.io/badge/license-MIT-FF4F00?style=flat-square&labelColor=000000" />
|
||||
<img alt="rust" src="https://img.shields.io/badge/rust-stable-FF4F00?style=flat-square&logo=rust&logoColor=white&labelColor=000000" />
|
||||
<img alt="tauri" src="https://img.shields.io/badge/tauri-v2-FF4F00?style=flat-square&logo=tauri&logoColor=white&labelColor=000000" />
|
||||
<img alt="solid-js" src="https://img.shields.io/badge/solid--js-1.9-FF4F00?style=flat-square&logo=solid&logoColor=white&labelColor=000000" />
|
||||
<img alt="p2p" src="https://img.shields.io/badge/libp2p-peer--to--peer-FF4F00?style=flat-square&logo=libp2p&logoColor=white&labelColor=000000" />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## what is dusk chat
|
||||
|
||||
|
|
@ -24,6 +38,155 @@ dusk chat is a decentralized alternative to discord. every user runs a full node
|
|||
- **rust**: for backend and relay server (https://rustup.rs)
|
||||
- **node**: for tauri cli (comes with bun)
|
||||
|
||||
### system dependencies
|
||||
|
||||
dusk chat is built on tauri v2, which requires platform-specific system libraries. install the dependencies for your OS before building.
|
||||
|
||||
#### linux
|
||||
|
||||
<details>
|
||||
<summary>debian / ubuntu</summary>
|
||||
|
||||
```bash
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.1-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libxdo-dev \
|
||||
libssl-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>arch / manjaro</summary>
|
||||
|
||||
```bash
|
||||
sudo pacman -Syu
|
||||
sudo pacman -S --needed \
|
||||
webkit2gtk-4.1 \
|
||||
base-devel \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
openssl \
|
||||
appmenu-gtk-module \
|
||||
libappindicator-gtk3 \
|
||||
librsvg \
|
||||
xdotool
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>fedora</summary>
|
||||
|
||||
```bash
|
||||
sudo dnf check-update
|
||||
sudo dnf install webkit2gtk4.1-devel \
|
||||
openssl-devel \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libappindicator-gtk3-devel \
|
||||
librsvg2-devel \
|
||||
libxdo-devel
|
||||
sudo dnf group install "c-development"
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>gentoo</summary>
|
||||
|
||||
```bash
|
||||
sudo emerge --ask \
|
||||
net-libs/webkit-gtk:4.1 \
|
||||
dev-libs/libappindicator \
|
||||
net-misc/curl \
|
||||
net-misc/wget \
|
||||
sys-apps/file
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>opensuse</summary>
|
||||
|
||||
```bash
|
||||
sudo zypper up
|
||||
sudo zypper in webkit2gtk3-devel \
|
||||
libopenssl-devel \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libappindicator3-1 \
|
||||
librsvg-devel
|
||||
sudo zypper in -t pattern devel_basis
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>alpine</summary>
|
||||
|
||||
```bash
|
||||
sudo apk add \
|
||||
build-base \
|
||||
webkit2gtk-4.1-dev \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
openssl \
|
||||
libayatana-appindicator-dev \
|
||||
librsvg
|
||||
```
|
||||
|
||||
note: alpine containers don't include fonts by default. install at least one font package (e.g. `font-dejavu`) for text to render correctly.
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>ostree (silverblue / kinoite)</summary>
|
||||
|
||||
```bash
|
||||
sudo rpm-ostree install webkit2gtk4.1-devel \
|
||||
openssl-devel \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libappindicator-gtk3-devel \
|
||||
librsvg2-devel \
|
||||
libxdo-devel \
|
||||
gcc \
|
||||
gcc-c++ \
|
||||
make
|
||||
sudo systemctl reboot
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>nixos</summary>
|
||||
|
||||
see the [nixos wiki page for tauri](https://wiki.nixos.org/wiki/Tauri).
|
||||
|
||||
</details>
|
||||
|
||||
#### macos
|
||||
|
||||
install [xcode](https://developer.apple.com/xcode/resources/) from the mac app store or the apple developer website. launch it once after installing so it finishes setup.
|
||||
|
||||
#### windows
|
||||
|
||||
1. install [microsoft c++ build tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and check "desktop development with c++" during setup
|
||||
2. webview2 is pre-installed on windows 10 (1803+) and windows 11. if you're on an older version, install the [webview2 runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)
|
||||
3. install rust via [rustup](https://rustup.rs) or `winget install --id Rustlang.Rustup` -- make sure the MSVC toolchain is selected as default
|
||||
|
||||
### installation
|
||||
|
||||
1. clone the repository
|
||||
|
|
|
|||
|
|
@ -181,6 +181,48 @@ fn community_id_from_topic(topic: &str) -> Option<&str> {
|
|||
pub type VoiceChannelMap =
|
||||
Arc<Mutex<HashMap<String, Vec<crate::protocol::messages::VoiceParticipant>>>>;
|
||||
|
||||
// build a signed profile announcement from the keypair and storage
|
||||
// used by the event loop to re-announce after relay connection or new peer joins
|
||||
fn build_profile_announcement(
|
||||
keypair: &libp2p::identity::Keypair,
|
||||
storage: &crate::storage::DiskStorage,
|
||||
) -> Option<crate::protocol::messages::ProfileAnnouncement> {
|
||||
let profile = storage.load_profile().ok()?;
|
||||
let proof = storage.load_verification_proof().ok().flatten();
|
||||
let peer_id = libp2p::PeerId::from(keypair.public());
|
||||
|
||||
let mut announcement = crate::protocol::messages::ProfileAnnouncement {
|
||||
peer_id: peer_id.to_string(),
|
||||
display_name: profile.display_name,
|
||||
bio: profile.bio,
|
||||
public_key: hex::encode(keypair.public().encode_protobuf()),
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
verification_proof: proof,
|
||||
signature: String::new(),
|
||||
};
|
||||
announcement.signature = verification::sign_announcement(keypair, &announcement);
|
||||
Some(announcement)
|
||||
}
|
||||
|
||||
// publish our profile on the directory gossipsub topic so connected peers
|
||||
// learn about us and add us to their local directory
|
||||
fn publish_profile(
|
||||
swarm: &mut libp2p::Swarm<behaviour::DuskBehaviour>,
|
||||
keypair: &libp2p::identity::Keypair,
|
||||
storage: &crate::storage::DiskStorage,
|
||||
) {
|
||||
if let Some(announcement) = build_profile_announcement(keypair, storage) {
|
||||
let msg = crate::protocol::messages::GossipMessage::ProfileAnnounce(announcement);
|
||||
if let Ok(data) = serde_json::to_vec(&msg) {
|
||||
let topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_directory());
|
||||
let _ = swarm.behaviour_mut().gossipsub.publish(topic, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// start the p2p node on a background task
|
||||
pub async fn start(
|
||||
keypair: libp2p::identity::Keypair,
|
||||
|
|
@ -230,6 +272,10 @@ pub async fn start(
|
|||
let _ = app_handle.emit("dusk-event", DuskEvent::RelayStatus { connected: false });
|
||||
}
|
||||
|
||||
// clone the keypair into the event loop so it can re-announce our profile
|
||||
// when new peers connect or the relay comes online
|
||||
let node_keypair = keypair;
|
||||
|
||||
let task = tauri::async_runtime::spawn(async move {
|
||||
use futures::StreamExt;
|
||||
|
||||
|
|
@ -308,6 +354,14 @@ pub async fn start(
|
|||
}
|
||||
crate::crdt::sync::SyncMessage::DocumentOffer(snapshot) => {
|
||||
let mut engine = crdt_engine.lock().await;
|
||||
|
||||
// only merge docs for communities we've explicitly joined or created,
|
||||
// otherwise any LAN peer would push all their communities to us
|
||||
if !engine.has_community(&snapshot.community_id) {
|
||||
log::debug!("ignoring document offer for unknown community {}", snapshot.community_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
match engine.merge_remote_doc(&snapshot.community_id, &snapshot.doc_bytes) {
|
||||
Ok(()) => {
|
||||
let _ = app_handle.emit("dusk-event", DuskEvent::SyncComplete {
|
||||
|
|
@ -571,7 +625,7 @@ pub async fn start(
|
|||
peer_count: connected_peers.len(),
|
||||
});
|
||||
|
||||
// sync documents with newly discovered LAN peers
|
||||
// sync documents and announce profile to newly discovered LAN peers
|
||||
if !peers.is_empty() {
|
||||
let local_peer_id = *swarm_instance.local_peer_id();
|
||||
let request = crate::crdt::sync::SyncMessage::RequestSync {
|
||||
|
|
@ -581,6 +635,8 @@ pub async fn start(
|
|||
let sync_topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_sync());
|
||||
let _ = swarm_instance.behaviour_mut().gossipsub.publish(sync_topic, data);
|
||||
}
|
||||
|
||||
publish_profile(&mut swarm_instance, &node_keypair, &storage);
|
||||
}
|
||||
}
|
||||
libp2p::swarm::SwarmEvent::Behaviour(behaviour::DuskBehaviourEvent::Mdns(
|
||||
|
|
@ -646,6 +702,12 @@ pub async fn start(
|
|||
|
||||
// queues drained, reset the TTL tracker
|
||||
pending_queued_at = None;
|
||||
|
||||
// re-announce our profile now that the relay is up
|
||||
// the initial announcement in start_node fires before
|
||||
// any WAN peers are reachable, so this ensures remote
|
||||
// peers learn about us once the relay mesh is live
|
||||
publish_profile(&mut swarm_instance, &node_keypair, &storage);
|
||||
}
|
||||
|
||||
// --- rendezvous client events ---
|
||||
|
|
@ -768,6 +830,13 @@ pub async fn start(
|
|||
let sync_topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_sync());
|
||||
let _ = swarm_instance.behaviour_mut().gossipsub.publish(sync_topic, data);
|
||||
}
|
||||
|
||||
// re-announce our profile so the new peer adds us to
|
||||
// their directory. skip the relay itself since it does
|
||||
// not participate in the gossipsub directory mesh.
|
||||
if Some(peer_id) != relay_peer {
|
||||
publish_profile(&mut swarm_instance, &node_keypair, &storage);
|
||||
}
|
||||
}
|
||||
libp2p::swarm::SwarmEvent::ConnectionClosed { peer_id, num_established, .. } => {
|
||||
if num_established == 0 {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import Button from "../common/Button";
|
|||
import Divider from "../common/Divider";
|
||||
import {
|
||||
knownPeers,
|
||||
setKnownPeers,
|
||||
markAsFriend,
|
||||
unmarkAsFriend,
|
||||
} from "../../stores/directory";
|
||||
|
|
@ -41,9 +42,15 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
|
|||
const [activeTab, setActiveTab] = createSignal<DirectoryTab>("all");
|
||||
const [copiedId, setCopiedId] = createSignal<string | null>(null);
|
||||
|
||||
// trigger global peer discovery when modal opens
|
||||
// reload directory from disk and trigger fresh discovery when modal opens
|
||||
createEffect(() => {
|
||||
if (props.isOpen) {
|
||||
// refresh the in-memory peer list from disk so any profiles received
|
||||
// while the modal was closed are visible immediately
|
||||
tauri.getKnownPeers().then((peers) => {
|
||||
setKnownPeers(peers);
|
||||
}).catch(() => {});
|
||||
|
||||
// discover peers registered on the relay's global "dusk/peers" namespace
|
||||
// this allows finding online peers without sharing a community
|
||||
tauri.discoverGlobalPeers().catch((e) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue