- 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>
99 lines
3.2 KiB
Rust
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;
|