//! Presence registry — tracks which fingerprints are connected to this relay //! and to peer relays (via gossip over probe connections). //! //! This enables route resolution: given a fingerprint, determine whether the //! user is local, on a known peer relay, or unknown. use std::collections::{HashMap, HashSet}; use std::net::SocketAddr; use std::time::{Duration, Instant}; use serde::Serialize; // --------------------------------------------------------------------------- // Data structures // --------------------------------------------------------------------------- /// Where a fingerprint is connected. #[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum PresenceLocation { /// Connected directly to this relay. Local, /// Connected to a peer relay at the given address. Remote(SocketAddr), } /// Presence entry for a fingerprint connected directly to this relay. #[derive(Clone, Debug)] pub struct LocalPresence { pub fingerprint: String, pub alias: Option, pub connected_at: Instant, pub room: Option, } /// Presence entry for a fingerprint reported by a peer relay. #[derive(Clone, Debug)] pub struct RemotePresence { pub fingerprint: String, pub relay_addr: SocketAddr, pub last_seen: Instant, } /// Known peer relay and its reported fingerprints. #[derive(Clone, Debug)] pub struct PeerRelay { pub addr: SocketAddr, pub fingerprints: HashSet, pub last_update: Instant, pub rtt_ms: Option, } // --------------------------------------------------------------------------- // Registry // --------------------------------------------------------------------------- /// Central presence registry tracking local and remote fingerprints. pub struct PresenceRegistry { /// Fingerprints connected directly to THIS relay. local: HashMap, /// Fingerprints reported by peer relays (via gossip). remote: HashMap, /// Known peer relays and their status. peers: HashMap, } impl PresenceRegistry { /// Create an empty registry. pub fn new() -> Self { Self { local: HashMap::new(), remote: HashMap::new(), peers: HashMap::new(), } } /// Register a fingerprint as locally connected (called after auth + handshake). pub fn register_local(&mut self, fingerprint: &str, alias: Option, room: Option) { self.local.insert(fingerprint.to_string(), LocalPresence { fingerprint: fingerprint.to_string(), alias, connected_at: Instant::now(), room, }); } /// Unregister a locally connected fingerprint (called on disconnect). pub fn unregister_local(&mut self, fingerprint: &str) { self.local.remove(fingerprint); } /// Update the fingerprints reported by a peer relay. /// Replaces the previous set for that peer. pub fn update_peer(&mut self, addr: SocketAddr, fingerprints: HashSet) { let now = Instant::now(); // Remove old remote entries that belonged to this peer self.remote.retain(|_, rp| rp.relay_addr != addr); // Insert new remote entries for fp in &fingerprints { self.remote.insert(fp.clone(), RemotePresence { fingerprint: fp.clone(), relay_addr: addr, last_seen: now, }); } // Update the peer record let peer = self.peers.entry(addr).or_insert_with(|| PeerRelay { addr, fingerprints: HashSet::new(), last_update: now, rtt_ms: None, }); peer.fingerprints = fingerprints; peer.last_update = now; } /// Look up where a fingerprint is connected. /// Local presence takes priority over remote. pub fn lookup(&self, fingerprint: &str) -> Option { if self.local.contains_key(fingerprint) { return Some(PresenceLocation::Local); } if let Some(rp) = self.remote.get(fingerprint) { return Some(PresenceLocation::Remote(rp.relay_addr)); } None } /// Return all fingerprints connected directly to this relay. pub fn local_fingerprints(&self) -> HashSet { self.local.keys().cloned().collect() } /// Return a full dump of every known fingerprint and its location. pub fn all_known(&self) -> Vec<(String, PresenceLocation)> { let mut out = Vec::new(); for fp in self.local.keys() { out.push((fp.clone(), PresenceLocation::Local)); } for (fp, rp) in &self.remote { // Skip if also local (local wins) if !self.local.contains_key(fp) { out.push((fp.clone(), PresenceLocation::Remote(rp.relay_addr))); } } out } /// Remove remote entries older than `timeout`. pub fn expire_stale(&mut self, timeout: Duration) { let cutoff = Instant::now() - timeout; // Expire remote presence entries self.remote.retain(|_, rp| rp.last_seen > cutoff); // Expire peer relay records and their fingerprint sets let stale_peers: Vec = self.peers .iter() .filter(|(_, p)| p.last_update <= cutoff) .map(|(addr, _)| *addr) .collect(); for addr in stale_peers { self.peers.remove(&addr); } } /// Return a reference to the peer relay map (for HTTP API). pub fn peers(&self) -> &HashMap { &self.peers } /// Return a reference to the local presence map (for HTTP API). pub fn local_entries(&self) -> &HashMap { &self.local } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use std::net::SocketAddr; fn addr(s: &str) -> SocketAddr { s.parse().unwrap() } #[test] fn register_and_lookup_local() { let mut reg = PresenceRegistry::new(); reg.register_local("aabbccdd", Some("alice".into()), Some("room1".into())); assert_eq!(reg.lookup("aabbccdd"), Some(PresenceLocation::Local)); // Unknown fingerprint returns None assert_eq!(reg.lookup("00000000"), None); } #[test] fn unregister_removes() { let mut reg = PresenceRegistry::new(); reg.register_local("aabbccdd", None, None); assert_eq!(reg.lookup("aabbccdd"), Some(PresenceLocation::Local)); reg.unregister_local("aabbccdd"); assert_eq!(reg.lookup("aabbccdd"), None); } #[test] fn update_peer_and_lookup() { let mut reg = PresenceRegistry::new(); let peer = addr("10.0.0.2:4433"); let mut fps = HashSet::new(); fps.insert("deadbeef".to_string()); fps.insert("cafebabe".to_string()); reg.update_peer(peer, fps); assert_eq!(reg.lookup("deadbeef"), Some(PresenceLocation::Remote(peer))); assert_eq!(reg.lookup("cafebabe"), Some(PresenceLocation::Remote(peer))); assert_eq!(reg.lookup("unknown"), None); } #[test] fn expire_stale_removes_old() { let mut reg = PresenceRegistry::new(); let peer = addr("10.0.0.3:4433"); let mut fps = HashSet::new(); fps.insert("olduser".to_string()); reg.update_peer(peer, fps); // Verify it's there assert_eq!(reg.lookup("olduser"), Some(PresenceLocation::Remote(peer))); // Manually backdate the last_seen and last_update if let Some(rp) = reg.remote.get_mut("olduser") { rp.last_seen = Instant::now() - Duration::from_secs(120); } if let Some(p) = reg.peers.get_mut(&peer) { p.last_update = Instant::now() - Duration::from_secs(120); } // Expire with 60s timeout — should remove the 120s-old entries reg.expire_stale(Duration::from_secs(60)); assert_eq!(reg.lookup("olduser"), None); assert!(reg.peers.get(&peer).is_none()); } #[test] fn local_fingerprints_list() { let mut reg = PresenceRegistry::new(); reg.register_local("fp1", None, None); reg.register_local("fp2", Some("bob".into()), Some("room-a".into())); reg.register_local("fp3", None, None); let fps = reg.local_fingerprints(); assert_eq!(fps.len(), 3); assert!(fps.contains("fp1")); assert!(fps.contains("fp2")); assert!(fps.contains("fp3")); } #[test] fn all_known_includes_local_and_remote() { let mut reg = PresenceRegistry::new(); reg.register_local("local1", None, None); let peer = addr("10.0.0.5:4433"); let mut fps = HashSet::new(); fps.insert("remote1".to_string()); reg.update_peer(peer, fps); let all = reg.all_known(); assert_eq!(all.len(), 2); let local_entries: Vec<_> = all.iter() .filter(|(_, loc)| *loc == PresenceLocation::Local) .collect(); assert_eq!(local_entries.len(), 1); assert_eq!(local_entries[0].0, "local1"); let remote_entries: Vec<_> = all.iter() .filter(|(_, loc)| matches!(loc, PresenceLocation::Remote(_))) .collect(); assert_eq!(remote_entries.len(), 1); assert_eq!(remote_entries[0].0, "remote1"); } #[test] fn local_overrides_remote_in_lookup() { let mut reg = PresenceRegistry::new(); let peer = addr("10.0.0.6:4433"); // Register as remote first let mut fps = HashSet::new(); fps.insert("dupfp".to_string()); reg.update_peer(peer, fps); assert_eq!(reg.lookup("dupfp"), Some(PresenceLocation::Remote(peer))); // Now register locally — local should win reg.register_local("dupfp", None, None); assert_eq!(reg.lookup("dupfp"), Some(PresenceLocation::Local)); } #[test] fn update_peer_replaces_old_fingerprints() { let mut reg = PresenceRegistry::new(); let peer = addr("10.0.0.7:4433"); let mut fps1 = HashSet::new(); fps1.insert("user_a".to_string()); fps1.insert("user_b".to_string()); reg.update_peer(peer, fps1); assert_eq!(reg.lookup("user_a"), Some(PresenceLocation::Remote(peer))); assert_eq!(reg.lookup("user_b"), Some(PresenceLocation::Remote(peer))); // Update with only user_b — user_a should be gone let mut fps2 = HashSet::new(); fps2.insert("user_b".to_string()); reg.update_peer(peer, fps2); assert_eq!(reg.lookup("user_a"), None); assert_eq!(reg.lookup("user_b"), Some(PresenceLocation::Remote(peer))); } }