Files
peerspeak/src/network/mod.rs
T
molluskandClaude Opus 4.8 7af0235736 chore: senior-review cleanup pass
- Remove Gemini's committed update_*.py regex-surgery scripts
- Drop unused iroh-tickets dependency (hand-rolled ticket is used instead)
- Replace ToString antipattern with Display impl on PeerSpeakTicket
- Route debug log to XDG state/cache dir instead of hardcoded /home path
- Clear all compiler + clippy warnings (unused imports, collapsible ifs,
  redundant pattern matching, missing Default)

Builds clean with zero warnings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-29 15:36:55 -04:00

99 lines
3.2 KiB
Rust

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<Self, Self::Err> {
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<Receiver<(EndpointId, Bytes)>, 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<Receiver<RoomEvent>, NetError>;
}
pub mod iroh_impl;
pub mod gossip;