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
|
## 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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue