chat_core/
stats.rs

1use std::collections::{HashMap, HashSet};
2
3#[derive(Debug, Clone, PartialEq)]
4pub enum ConnectionStatus {
5    Online,
6    Offline,
7    Connecting,
8}
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct ConnectionStats {
12    pub peer_id: String,
13    pub status: ConnectionStatus,
14    pub bootstrap_connections: u32,
15    pub direct_peers: u32,
16    pub relay_connections: u32,
17    pub active_transports: Vec<String>,
18    pub protocols: Vec<String>,
19    pub connected_peers: Vec<String>,
20    pub room_peers: Vec<String>,
21    pub peer_transports: HashMap<String, Vec<String>>,
22    pub peer_addresses: HashMap<String, Vec<String>>,
23    pub circuit_address: Option<String>,
24    pub web_rtc_address: Option<String>,
25}
26
27/// Raw network data returned by [`NetworkNode::raw_stats`](crate::network::NetworkNode).
28#[derive(Debug, Clone)]
29pub struct RawStats {
30    pub peer_id: String,
31    pub num_peers: u32,
32    pub relay_count: u32,
33    pub listeners: Vec<String>,
34    pub external_addresses: Vec<String>,
35    pub connected_peers: Vec<String>,
36    pub peer_conn_addrs: HashMap<String, Vec<String>>,
37    pub peer_advertised_addrs: HashMap<String, Vec<String>>,
38    pub circuit_address: Option<String>,
39    pub web_rtc_address: Option<String>,
40}
41
42/// Build a [`ConnectionStats`] from [`RawStats`] and an optional room-peers override.
43pub fn build_connection_stats(raw: RawStats, room_peers: Vec<String>) -> ConnectionStats {
44    let direct = raw.num_peers.saturating_sub(raw.relay_count);
45
46    let mut transports = HashSet::new();
47    for addr in raw.listeners.iter().chain(raw.external_addresses.iter()) {
48        let name = transport_name(addr);
49        if name != "unknown" {
50            transports.insert(name.to_string());
51        }
52    }
53
54    let mut peer_transports = HashMap::new();
55    let mut peer_addresses = HashMap::new();
56    for peer_id in &raw.connected_peers {
57        let conn_addrs = raw.peer_conn_addrs.get(peer_id).cloned().unwrap_or_default();
58        let mut seen = HashSet::new();
59        let mut names = Vec::new();
60        for addr in &conn_addrs {
61            let name = transport_name(addr);
62            if name != "unknown" && seen.insert(name) {
63                names.push(name.to_string());
64            }
65        }
66        // If active connection addresses yield no recognizable transport,
67        // fall back to the peer's advertised listen addresses (Identify).
68        if (names.is_empty() || (names.len() == 1 && names[0] == "unknown"))
69            && let Some(advertised) = raw.peer_advertised_addrs.get(peer_id)
70        {
71            for addr in advertised {
72                let name = transport_name(addr);
73                if name != "unknown" && seen.insert(name) {
74                    names.push(name.to_string());
75                }
76            }
77        }
78        // Strip useless "unknown" placeholder before sending to UI.
79        names.retain(|n| n != "unknown");
80        peer_transports.insert(peer_id.clone(), names);
81        let addrs = raw.peer_advertised_addrs.get(peer_id).cloned().unwrap_or_default();
82        let useful_addrs: Vec<String> = addrs.into_iter().filter(|a| addr_has_transport(a)).collect();
83        peer_addresses.insert(peer_id.clone(), useful_addrs);
84    }
85
86    ConnectionStats {
87        peer_id: raw.peer_id,
88        status: if raw.num_peers > 0 {
89            ConnectionStatus::Online
90        } else {
91            ConnectionStatus::Offline
92        },
93        bootstrap_connections: 0,
94        direct_peers: direct,
95        relay_connections: raw.relay_count,
96        active_transports: transports.into_iter().collect(),
97        protocols: vec![],
98        connected_peers: raw.connected_peers,
99        room_peers,
100        peer_transports,
101        peer_addresses,
102        circuit_address: raw.circuit_address,
103        web_rtc_address: raw.web_rtc_address,
104    }
105}
106
107// ------------------------------------------------------------------
108// Minimal transport helpers (kept in core so build_connection_stats
109// can compute transport names without depending on libp2p internals).
110// ------------------------------------------------------------------
111
112/// Whether a multiaddr string contains at least one transport protocol.
113/// Used to reject bare `/p2p/<peer_id>` addresses that have no value for
114/// display or dialing.
115pub fn addr_has_transport(addr: &str) -> bool {
116    addr.contains("webrtc")
117        || addr.contains("webtransport")
118        || addr.contains("p2p-circuit")
119        || addr.contains("wss")
120        || addr.contains("ws")
121        || addr.contains("tcp")
122        || addr.contains("udp")
123        || addr.contains("quic")
124        || addr.contains("ip4")
125        || addr.contains("ip6")
126        || addr.contains("dns4")
127        || addr.contains("dns6")
128        || addr.contains("dns")
129}
130
131/// Return a transport keyword for a multiaddr string.
132///
133/// For circuit-relay addresses the *final* transport (after the relay hop) is
134/// reported so the UI shows how the target peer is reachable, not how the
135/// relay is reached.
136pub fn transport_name(addr: &str) -> &'static str {
137    // Circuit-relay addresses may contain an underlying transport name
138    // (e.g. webtransport to the relay). We only want to report the
139    // direct peer transport, not the hop to the relay.
140    if let Some(circuit_pos) = addr.find("p2p-circuit") {
141        if let Some(webrtc_pos) = addr.find("webrtc")
142            && webrtc_pos > circuit_pos
143        {
144            return "webrtc";
145        }
146        if let Some(wt_pos) = addr.find("webtransport")
147            && wt_pos > circuit_pos
148        {
149            return "webtransport";
150        }
151        return "p2p-circuit";
152    }
153
154    if addr.contains("webrtc-direct") {
155        "webrtc-direct"
156    } else if addr.contains("webrtc") {
157        "webrtc"
158    } else if addr.contains("webtransport") {
159        "webtransport"
160    } else if addr.contains("wss") || addr.contains("ws") {
161        "websocket"
162    } else if addr.contains("tcp") {
163        "tcp"
164    } else if addr.contains("quic") {
165        "quic"
166    } else {
167        "unknown"
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn transport_name_for_webrtc() {
177        let addr = "/dns4/example.com/tcp/443/wss/p2p-circuit/webrtc/p2p/12D3KooWGz9xtYbQ8UYj4oRmx4pD3vZvq6T5zN4pMjz6p6V7X8Y9";
178        assert_eq!(transport_name(addr), "webrtc");
179    }
180
181    #[test]
182    fn transport_name_for_wss() {
183        let addr = "/dns4/example.com/tcp/443/wss";
184        assert_eq!(transport_name(addr), "websocket");
185    }
186
187    #[test]
188    fn transport_name_for_tcp() {
189        let addr = "/ip4/127.0.0.1/tcp/4001";
190        assert_eq!(transport_name(addr), "tcp");
191    }
192
193    #[test]
194    fn transport_name_for_quic() {
195        let addr = "/ip4/127.0.0.1/udp/4001/quic-v1";
196        assert_eq!(transport_name(addr), "quic");
197    }
198
199    #[test]
200    fn transport_name_for_webrtc_direct() {
201        let addr = "/ip4/127.0.0.1/udp/4001/webrtc-direct/certhash/uEiA...";
202        assert_eq!(transport_name(addr), "webrtc-direct");
203    }
204
205    #[test]
206    fn transport_name_for_relay() {
207        let addr = "/dns4/example.com/tcp/443/wss/p2p-circuit/p2p/12D3KooWGz9xtYbQ8UYj4oRmx4pD3vZvq6T5zN4pMjz6p6V7X8Y9";
208        assert_eq!(transport_name(addr), "p2p-circuit");
209    }
210
211    #[test]
212    fn transport_name_for_circuit_over_webtransport() {
213        let addr = "/dns4/relay.example.com/udp/443/quic/webtransport/p2p/12D3KooWRelay/p2p-circuit/p2p/12D3KooWTarget";
214        assert_eq!(transport_name(addr), "p2p-circuit");
215    }
216
217    #[test]
218    fn transport_name_for_circuit_over_webrtc_signaling() {
219        let addr = "/dns4/relay.example.com/tcp/443/wss/p2p-circuit/webrtc/p2p/12D3KooWTarget";
220        assert_eq!(transport_name(addr), "webrtc");
221    }
222
223    #[test]
224    fn bare_peer_id_has_no_transport() {
225        let addr = "/p2p/12D3KooWKLKynHuhH8v2QNgfoj728nZLP83GUZpzeWX8um4VrVGa";
226        assert!(!addr_has_transport(addr));
227    }
228
229    #[test]
230    fn full_multiaddr_has_transport() {
231        let addr = "/ip4/127.0.0.1/tcp/4001/p2p/12D3KooWKLKynHuhH8v2QNgfoj728nZLP83GUZpzeWX8um4VrVGa";
232        assert!(addr_has_transport(addr));
233    }
234}