use iroh::EndpointId; use bytes::Bytes; use thiserror::Error; use tokio::sync::mpsc::Receiver; use async_trait::async_trait; use serde::{Serialize, Deserialize}; use std::str::FromStr; #[derive(Error, Debug)] pub enum NetError { #[error("Failed to initialize network: {0}")] Init(String), #[error("Failed to connect/dial peer: {0}")] Connection(String), #[error("Gossip swarm error: {0}")] Gossip(String), #[error("Serialization / Deserialization error: {0}")] Serde(String), #[error("Invalid ticket: {0}")] InvalidTicket(String), #[error("Other network error: {0}")] Other(String), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct PeerState { pub name: String, pub is_muted: bool, pub addr: iroh::EndpointAddr, } #[derive(Debug, Clone)] pub enum RoomEvent { PeerJoined(EndpointId, PeerState), PeerLeft(EndpointId), PeerUpdated(EndpointId, PeerState), } #[derive(Serialize, Deserialize, Clone, Debug)] pub struct PeerSpeakTicket { pub host_addr: iroh::EndpointAddr, pub topic_id: [u8; 32], } impl std::fmt::Display for PeerSpeakTicket { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // serde_json on a struct of String/[u8;32] fields is infallible in practice, // but Display can't surface an error, so fall back to an empty ticket body. let serialized = serde_json::to_vec(self).unwrap_or_default(); let encoded = base64::Engine::encode( &base64::engine::general_purpose::URL_SAFE_NO_PAD, &serialized, ); f.write_str(&encoded) } } impl FromStr for PeerSpeakTicket { type Err = NetError; fn from_str(s: &str) -> Result { let decoded = base64::Engine::decode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, s) .map_err(|e| NetError::InvalidTicket(e.to_string()))?; let ticket: PeerSpeakTicket = serde_json::from_slice(&decoded) .map_err(|e| NetError::InvalidTicket(e.to_string()))?; Ok(ticket) } } #[async_trait] pub trait NetworkTransport: Send + Sync { /// Send a low-latency unreliable datagram to a specific peer (for audio). async fn send_datagram(&self, peer_id: EndpointId, data: Bytes) -> Result<(), NetError>; /// Subscribes to incoming datagrams from any peer. async fn receive_datagrams(&self) -> Result, NetError>; } #[async_trait] pub trait RoomState: Send + Sync { /// Joins a room using a gossip ticket string and announces our state. async fn join(&self, ticket: &str, self_state: PeerState) -> Result<(), NetError>; /// Updates our local state (e.g. when user mutes/unmutes) and broadcasts it. async fn update_self_state(&self, self_state: PeerState) -> Result<(), NetError>; /// Leaves the room and announces departure. async fn leave(&self) -> Result<(), NetError>; /// Returns a list of currently active peers in the room. fn active_peers(&self) -> Vec<(EndpointId, PeerState)>; /// Subscribes to room events (peer joined, peer left, peer updated). async fn subscribe_events(&self) -> Result, NetError>; } pub mod iroh_impl; pub mod gossip;