From 1940bb3fbf79cbf99388bf0a07e7b8914dd2d743 Mon Sep 17 00:00:00 2001 From: cloudwithax Date: Sun, 15 Feb 2026 10:51:34 -0500 Subject: [PATCH] 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 --- README.md | 167 +++++++++++++++++- src-tauri/src/node/mod.rs | 71 +++++++- .../directory/UserDirectoryModal.tsx | 9 +- 3 files changed, 243 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0b4880e..052e140 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ -# dusk chat +
+ dusk chat -a peer-to-peer community chat platform. no central server. your data stays yours. +

dusk chat

+

a peer-to-peer community chat platform. no central server. your data stays yours.

+ +

+ version + license + rust + tauri + solid-js + p2p +

+
+ +--- ## 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 + +
+debian / ubuntu + +```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 +``` + +
+ +
+arch / manjaro + +```bash +sudo pacman -Syu +sudo pacman -S --needed \ + webkit2gtk-4.1 \ + base-devel \ + curl \ + wget \ + file \ + openssl \ + appmenu-gtk-module \ + libappindicator-gtk3 \ + librsvg \ + xdotool +``` + +
+ +
+fedora + +```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" +``` + +
+ +
+gentoo + +```bash +sudo emerge --ask \ + net-libs/webkit-gtk:4.1 \ + dev-libs/libappindicator \ + net-misc/curl \ + net-misc/wget \ + sys-apps/file +``` + +
+ +
+opensuse + +```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 +``` + +
+ +
+alpine + +```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. + +
+ +
+ostree (silverblue / kinoite) + +```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 +``` + +
+ +
+nixos + +see the [nixos wiki page for tauri](https://wiki.nixos.org/wiki/Tauri). + +
+ +#### 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 diff --git a/src-tauri/src/node/mod.rs b/src-tauri/src/node/mod.rs index c3513d1..aeaf4b0 100644 --- a/src-tauri/src/node/mod.rs +++ b/src-tauri/src/node/mod.rs @@ -181,6 +181,48 @@ fn community_id_from_topic(topic: &str) -> Option<&str> { pub type VoiceChannelMap = Arc>>>; +// 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 { + 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, + 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 { diff --git a/src/components/directory/UserDirectoryModal.tsx b/src/components/directory/UserDirectoryModal.tsx index 60e2583..fb174b7 100644 --- a/src/components/directory/UserDirectoryModal.tsx +++ b/src/components/directory/UserDirectoryModal.tsx @@ -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 = (props) => { const [activeTab, setActiveTab] = createSignal("all"); const [copiedId, setCopiedId] = createSignal(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) => {