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:
cloudwithax 2026-02-15 10:51:34 -05:00
parent ed7e6d6239
commit 1940bb3fbf
3 changed files with 243 additions and 4 deletions

167
README.md
View File

@ -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 ## 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) - **rust**: for backend and relay server (https://rustup.rs)
- **node**: for tauri cli (comes with bun) - **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 ### installation
1. clone the repository 1. clone the repository

View File

@ -181,6 +181,48 @@ fn community_id_from_topic(topic: &str) -> Option<&str> {
pub type VoiceChannelMap = pub type VoiceChannelMap =
Arc<Mutex<HashMap<String, Vec<crate::protocol::messages::VoiceParticipant>>>>; 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 // start the p2p node on a background task
pub async fn start( pub async fn start(
keypair: libp2p::identity::Keypair, keypair: libp2p::identity::Keypair,
@ -230,6 +272,10 @@ pub async fn start(
let _ = app_handle.emit("dusk-event", DuskEvent::RelayStatus { connected: false }); 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 { let task = tauri::async_runtime::spawn(async move {
use futures::StreamExt; use futures::StreamExt;
@ -308,6 +354,14 @@ pub async fn start(
} }
crate::crdt::sync::SyncMessage::DocumentOffer(snapshot) => { crate::crdt::sync::SyncMessage::DocumentOffer(snapshot) => {
let mut engine = crdt_engine.lock().await; 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) { match engine.merge_remote_doc(&snapshot.community_id, &snapshot.doc_bytes) {
Ok(()) => { Ok(()) => {
let _ = app_handle.emit("dusk-event", DuskEvent::SyncComplete { let _ = app_handle.emit("dusk-event", DuskEvent::SyncComplete {
@ -571,7 +625,7 @@ pub async fn start(
peer_count: connected_peers.len(), 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() { if !peers.is_empty() {
let local_peer_id = *swarm_instance.local_peer_id(); let local_peer_id = *swarm_instance.local_peer_id();
let request = crate::crdt::sync::SyncMessage::RequestSync { 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 sync_topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_sync());
let _ = swarm_instance.behaviour_mut().gossipsub.publish(sync_topic, data); 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( libp2p::swarm::SwarmEvent::Behaviour(behaviour::DuskBehaviourEvent::Mdns(
@ -646,6 +702,12 @@ pub async fn start(
// queues drained, reset the TTL tracker // queues drained, reset the TTL tracker
pending_queued_at = None; 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 --- // --- rendezvous client events ---
@ -768,6 +830,13 @@ pub async fn start(
let sync_topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_sync()); let sync_topic = libp2p::gossipsub::IdentTopic::new(gossip::topic_for_sync());
let _ = swarm_instance.behaviour_mut().gossipsub.publish(sync_topic, data); 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, .. } => { libp2p::swarm::SwarmEvent::ConnectionClosed { peer_id, num_established, .. } => {
if num_established == 0 { if num_established == 0 {

View File

@ -21,6 +21,7 @@ import Button from "../common/Button";
import Divider from "../common/Divider"; import Divider from "../common/Divider";
import { import {
knownPeers, knownPeers,
setKnownPeers,
markAsFriend, markAsFriend,
unmarkAsFriend, unmarkAsFriend,
} from "../../stores/directory"; } from "../../stores/directory";
@ -41,9 +42,15 @@ const UserDirectoryModal: Component<UserDirectoryModalProps> = (props) => {
const [activeTab, setActiveTab] = createSignal<DirectoryTab>("all"); const [activeTab, setActiveTab] = createSignal<DirectoryTab>("all");
const [copiedId, setCopiedId] = createSignal<string | null>(null); 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(() => { createEffect(() => {
if (props.isOpen) { 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 // discover peers registered on the relay's global "dusk/peers" namespace
// this allows finding online peers without sharing a community // this allows finding online peers without sharing a community
tauri.discoverGlobalPeers().catch((e) => { tauri.discoverGlobalPeers().catch((e) => {