diff --git a/README.md b/README.md
index 0b4880e..052e140 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,20 @@
-# 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.
+
+
+
+
+
+
+
+
+
+
+
+---
## 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) => {