Skip to main content

hashtree_cli/webrtc/
types.rs

1//! WebRTC signaling types compatible with iris-client and hashtree-ts
2
3pub use hashtree_webrtc::{
4    decrement_htl_with_policy, should_forward_htl, validate_mesh_frame, HtlMode, HtlPolicy,
5    MeshNostrFrame, MeshNostrPayload, PeerHTLConfig, RequestDispatchConfig, SelectionStrategy,
6    TimedSeenSet, BLOB_REQUEST_POLICY, DECREMENT_AT_MAX_PROB, DECREMENT_AT_MIN_PROB, MAX_HTL,
7    MESH_DEFAULT_HTL, MESH_EVENT_POLICY, MESH_MAX_HTL, MESH_PROTOCOL, MESH_PROTOCOL_VERSION,
8};
9use serde::{Deserialize, Serialize};
10
11/// Backward-compatible helper using blob-request policy.
12pub fn decrement_htl(htl: u8, config: &PeerHTLConfig) -> u8 {
13    decrement_htl_with_policy(htl, &BLOB_REQUEST_POLICY, config)
14}
15
16/// Backward-compatible helper for existing call sites.
17pub fn should_forward(htl: u8) -> bool {
18    should_forward_htl(htl)
19}
20
21/// Event kind for WebRTC signaling (ephemeral kind 25050)
22/// All signaling uses this kind - hellos use #l tag, directed use gift wrap
23pub const WEBRTC_KIND: u64 = 25050;
24
25/// Tag for hello messages (broadcast discovery)
26pub const HELLO_TAG: &str = "hello";
27
28/// Legacy tag for WebRTC signaling messages (kept for compatibility)
29pub const WEBRTC_TAG: &str = "webrtc";
30
31/// Generate a UUID for peer identification
32pub fn generate_uuid() -> String {
33    use rand::Rng;
34    let mut rng = rand::thread_rng();
35    format!(
36        "{}{}",
37        (0..15)
38            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
39            .collect::<String>(),
40        (0..15)
41            .map(|_| char::from_digit(rng.gen_range(0..36), 36).unwrap())
42            .collect::<String>()
43    )
44}
45
46fn configured_peer_uuid() -> Option<String> {
47    std::env::var("HTREE_PEER_UUID")
48        .ok()
49        .map(|value| value.trim().to_string())
50        .filter(|value| !value.is_empty())
51}
52
53/// Peer identifier combining pubkey and session UUID
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct PeerId {
56    pub pubkey: String,
57    pub uuid: String,
58}
59
60impl PeerId {
61    pub fn new(pubkey: String, uuid: Option<String>) -> Self {
62        Self {
63            pubkey,
64            uuid: uuid
65                .or_else(configured_peer_uuid)
66                .unwrap_or_else(generate_uuid),
67        }
68    }
69
70    pub fn from_string(s: &str) -> Option<Self> {
71        let parts: Vec<&str> = s.split(':').collect();
72        if parts.len() == 2 {
73            Some(Self {
74                pubkey: parts[0].to_string(),
75                uuid: parts[1].to_string(),
76            })
77        } else {
78            None
79        }
80    }
81
82    pub fn short(&self) -> String {
83        format!(
84            "{}:{}",
85            &self.pubkey[..8.min(self.pubkey.len())],
86            &self.uuid[..6.min(self.uuid.len())]
87        )
88    }
89}
90
91impl std::fmt::Display for PeerId {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        write!(f, "{}:{}", self.pubkey, self.uuid)
94    }
95}
96
97/// Hello message for peer discovery
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct HelloMessage {
100    #[serde(rename = "type")]
101    pub msg_type: String,
102    #[serde(rename = "peerId")]
103    pub peer_id: String,
104}
105
106/// WebRTC offer message
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct OfferMessage {
109    #[serde(rename = "type")]
110    pub msg_type: String,
111    pub offer: serde_json::Value,
112    pub recipient: String,
113    #[serde(rename = "peerId")]
114    pub peer_id: String,
115}
116
117/// WebRTC answer message
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct AnswerMessage {
120    #[serde(rename = "type")]
121    pub msg_type: String,
122    pub answer: serde_json::Value,
123    pub recipient: String,
124    #[serde(rename = "peerId")]
125    pub peer_id: String,
126}
127
128/// ICE candidate message
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct CandidateMessage {
131    #[serde(rename = "type")]
132    pub msg_type: String,
133    pub candidate: serde_json::Value,
134    pub recipient: String,
135    #[serde(rename = "peerId")]
136    pub peer_id: String,
137}
138
139/// Batched ICE candidates message (hashtree-ts extension)
140#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct CandidatesMessage {
142    #[serde(rename = "type")]
143    pub msg_type: String,
144    pub candidates: Vec<serde_json::Value>,
145    pub recipient: String,
146    #[serde(rename = "peerId")]
147    pub peer_id: String,
148}
149
150/// All signaling message types
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(tag = "type")]
153pub enum SignalingMessage {
154    #[serde(rename = "hello")]
155    Hello {
156        #[serde(rename = "peerId")]
157        peer_id: String,
158    },
159    #[serde(rename = "offer")]
160    Offer {
161        offer: serde_json::Value,
162        recipient: String,
163        #[serde(rename = "peerId")]
164        peer_id: String,
165    },
166    #[serde(rename = "answer")]
167    Answer {
168        answer: serde_json::Value,
169        recipient: String,
170        #[serde(rename = "peerId")]
171        peer_id: String,
172    },
173    #[serde(rename = "candidate")]
174    Candidate {
175        candidate: serde_json::Value,
176        recipient: String,
177        #[serde(rename = "peerId")]
178        peer_id: String,
179    },
180    #[serde(rename = "candidates")]
181    Candidates {
182        candidates: Vec<serde_json::Value>,
183        recipient: String,
184        #[serde(rename = "peerId")]
185        peer_id: String,
186    },
187}
188
189impl SignalingMessage {
190    pub fn msg_type(&self) -> &str {
191        match self {
192            SignalingMessage::Hello { .. } => "hello",
193            SignalingMessage::Offer { .. } => "offer",
194            SignalingMessage::Answer { .. } => "answer",
195            SignalingMessage::Candidate { .. } => "candidate",
196            SignalingMessage::Candidates { .. } => "candidates",
197        }
198    }
199
200    pub fn recipient(&self) -> Option<&str> {
201        match self {
202            SignalingMessage::Hello { .. } => None,
203            SignalingMessage::Offer { recipient, .. } => Some(recipient),
204            SignalingMessage::Answer { recipient, .. } => Some(recipient),
205            SignalingMessage::Candidate { recipient, .. } => Some(recipient),
206            SignalingMessage::Candidates { recipient, .. } => Some(recipient),
207        }
208    }
209
210    pub fn peer_id(&self) -> &str {
211        match self {
212            SignalingMessage::Hello { peer_id } => peer_id,
213            SignalingMessage::Offer { peer_id, .. } => peer_id,
214            SignalingMessage::Answer { peer_id, .. } => peer_id,
215            SignalingMessage::Candidate { peer_id, .. } => peer_id,
216            SignalingMessage::Candidates { peer_id, .. } => peer_id,
217        }
218    }
219
220    pub fn hello(peer_id: &str) -> Self {
221        SignalingMessage::Hello {
222            peer_id: peer_id.to_string(),
223        }
224    }
225
226    pub fn offer(offer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
227        SignalingMessage::Offer {
228            offer,
229            recipient: recipient.to_string(),
230            peer_id: peer_id.to_string(),
231        }
232    }
233
234    pub fn answer(answer: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
235        SignalingMessage::Answer {
236            answer,
237            recipient: recipient.to_string(),
238            peer_id: peer_id.to_string(),
239        }
240    }
241
242    pub fn candidate(candidate: serde_json::Value, recipient: &str, peer_id: &str) -> Self {
243        SignalingMessage::Candidate {
244            candidate,
245            recipient: recipient.to_string(),
246            peer_id: peer_id.to_string(),
247        }
248    }
249}
250
251/// Configuration for WebRTC manager
252#[derive(Clone)]
253pub struct WebRTCConfig {
254    /// Nostr relays for signaling
255    pub relays: Vec<String>,
256    /// Whether negotiated WebRTC signaling should run at all.
257    pub signaling_enabled: bool,
258    /// Maximum outbound connections (legacy, use pools instead)
259    pub max_outbound: usize,
260    /// Maximum inbound connections (legacy, use pools instead)
261    pub max_inbound: usize,
262    /// Hello message interval in milliseconds
263    pub hello_interval_ms: u64,
264    /// Message timeout in milliseconds
265    pub message_timeout_ms: u64,
266    /// STUN servers for NAT traversal
267    pub stun_servers: Vec<String>,
268    /// Enable debug logging
269    pub debug: bool,
270    /// Optional LAN multicast transport for offline discovery + root lookup.
271    pub multicast: super::multicast::MulticastConfig,
272    /// Optional Android Wi-Fi Aware nearby discovery/signaling bus.
273    pub wifi_aware: super::wifi_aware::WifiAwareConfig,
274    /// Optional native Bluetooth peer transport.
275    pub bluetooth: super::bluetooth::BluetoothConfig,
276    /// Pool settings for follows and other peers
277    pub pools: PoolSettings,
278    /// Retrieval peer selection strategy (shared with simulation).
279    pub request_selection_strategy: SelectionStrategy,
280    /// Whether fairness constraints are enabled for retrieval peer selection.
281    pub request_fairness_enabled: bool,
282    /// Hedged request dispatch policy for retrieval (shared with simulation).
283    pub request_dispatch: RequestDispatchConfig,
284}
285
286impl Default for WebRTCConfig {
287    fn default() -> Self {
288        Self {
289            relays: vec![
290                "wss://relay.damus.io".to_string(),
291                "wss://relay.primal.net".to_string(),
292                "wss://nos.lol".to_string(),
293                "wss://temp.iris.to".to_string(),
294                "wss://relay.snort.social".to_string(),
295            ],
296            signaling_enabled: true,
297            max_outbound: 6,
298            max_inbound: 6,
299            hello_interval_ms: 3000,
300            message_timeout_ms: 15000,
301            stun_servers: vec![
302                "stun:stun.iris.to:3478".to_string(),
303                "stun:stun.l.google.com:19302".to_string(),
304                "stun:stun.cloudflare.com:3478".to_string(),
305            ],
306            debug: false,
307            multicast: super::multicast::MulticastConfig::default(),
308            wifi_aware: super::wifi_aware::WifiAwareConfig::default(),
309            bluetooth: super::bluetooth::BluetoothConfig::default(),
310            pools: PoolSettings::default(),
311            request_selection_strategy: SelectionStrategy::TitForTat,
312            request_fairness_enabled: true,
313            request_dispatch: RequestDispatchConfig {
314                initial_fanout: 2,
315                hedge_fanout: 1,
316                max_fanout: 8,
317                hedge_interval_ms: 120,
318            },
319        }
320    }
321}
322
323pub type PeerRouterConfig = WebRTCConfig;
324
325/// Peer connection status
326#[derive(Debug, Clone)]
327pub struct PeerStatus {
328    pub peer_id: String,
329    pub pubkey: String,
330    pub state: String,
331    pub direction: PeerDirection,
332    pub connected_at: Option<std::time::Instant>,
333    pub pool: PeerPool,
334}
335
336/// Direction of peer connection
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum PeerDirection {
339    Inbound,
340    Outbound,
341}
342
343/// Peer state change event for signaling layer notification
344#[derive(Debug, Clone)]
345pub enum PeerStateEvent {
346    /// Peer connection succeeded
347    Connected(PeerId),
348    /// Peer connection failed
349    Failed(PeerId),
350    /// Peer disconnected
351    Disconnected(PeerId),
352}
353
354/// Pool type for peer classification
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
356pub enum PeerPool {
357    /// Users in social graph (followed or followers)
358    Follows,
359    /// Everyone else
360    Other,
361}
362
363/// Configuration for a peer pool
364#[derive(Debug, Clone, Copy)]
365pub struct PoolConfig {
366    /// Maximum connections in this pool
367    pub max_connections: usize,
368    /// Number of connections to consider "satisfied" (stop sending hellos)
369    pub satisfied_connections: usize,
370}
371
372impl Default for PoolConfig {
373    fn default() -> Self {
374        Self {
375            max_connections: 16,
376            satisfied_connections: 8,
377        }
378    }
379}
380
381/// Pool settings for both pools
382#[derive(Debug, Clone)]
383pub struct PoolSettings {
384    pub follows: PoolConfig,
385    pub other: PoolConfig,
386}
387
388impl Default for PoolSettings {
389    fn default() -> Self {
390        Self {
391            follows: PoolConfig {
392                max_connections: 16,
393                satisfied_connections: 8,
394            },
395            other: PoolConfig {
396                max_connections: 16,
397                satisfied_connections: 8,
398            },
399        }
400    }
401}
402
403impl std::fmt::Display for PeerDirection {
404    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405        match self {
406            PeerDirection::Inbound => write!(f, "inbound"),
407            PeerDirection::Outbound => write!(f, "outbound"),
408        }
409    }
410}
411
412/// Message type bytes (prefix before MessagePack body)
413pub const MSG_TYPE_REQUEST: u8 = 0x00;
414pub const MSG_TYPE_RESPONSE: u8 = 0x01;
415pub const MSG_TYPE_QUOTE_REQUEST: u8 = 0x02;
416pub const MSG_TYPE_QUOTE_RESPONSE: u8 = 0x03;
417pub const MSG_TYPE_PAYMENT: u8 = 0x04;
418pub const MSG_TYPE_PAYMENT_ACK: u8 = 0x05;
419pub const MSG_TYPE_CHUNK: u8 = 0x06;
420
421/// Hashtree data channel protocol messages
422/// Shared between WebRTC data channels and WebSocket transport
423///
424/// Wire format: [type byte][msgpack body]
425/// Request:  [0x00][msgpack: {h: bytes32, htl?: u8, q?: u64}]
426/// Response: [0x01][msgpack: {h: bytes32, d: bytes}]
427/// QuoteRequest:  [0x02][msgpack: {h: bytes32, p: u64, t: u32, m?: string}]
428/// QuoteResponse: [0x03][msgpack: {h: bytes32, a: bool, q?: u64, p?: u64, t?: u32, m?: string}]
429/// Payment:       [0x04][msgpack: {h: bytes32, q: u64, c: u32, p: u64, m?: string, tok: string}]
430/// PaymentAck:    [0x05][msgpack: {h: bytes32, q: u64, c: u32, a: bool, e?: string}]
431/// Chunk:         [0x06][msgpack: {h: bytes32, q: u64, c: u32, n: u32, p: u64, d: bytes}]
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct DataRequest {
435    #[serde(with = "serde_bytes")]
436    pub h: Vec<u8>, // 32-byte hash
437    #[serde(default = "default_htl", skip_serializing_if = "is_max_htl")]
438    pub htl: u8,
439    #[serde(skip_serializing_if = "Option::is_none")]
440    pub q: Option<u64>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
444pub struct DataResponse {
445    #[serde(with = "serde_bytes")]
446    pub h: Vec<u8>, // 32-byte hash
447    #[serde(with = "serde_bytes")]
448    pub d: Vec<u8>, // Data
449}
450
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct DataQuoteRequest {
453    #[serde(with = "serde_bytes")]
454    pub h: Vec<u8>,
455    pub p: u64,
456    pub t: u32,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub m: Option<String>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct DataQuoteResponse {
463    #[serde(with = "serde_bytes")]
464    pub h: Vec<u8>,
465    pub a: bool,
466    #[serde(skip_serializing_if = "Option::is_none")]
467    pub q: Option<u64>,
468    #[serde(skip_serializing_if = "Option::is_none")]
469    pub p: Option<u64>,
470    #[serde(skip_serializing_if = "Option::is_none")]
471    pub t: Option<u32>,
472    #[serde(skip_serializing_if = "Option::is_none")]
473    pub m: Option<String>,
474}
475
476#[derive(Debug, Clone, Serialize, Deserialize)]
477pub struct DataPayment {
478    #[serde(with = "serde_bytes")]
479    pub h: Vec<u8>,
480    pub q: u64,
481    pub c: u32,
482    pub p: u64,
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub m: Option<String>,
485    pub tok: String,
486}
487
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct DataPaymentAck {
490    #[serde(with = "serde_bytes")]
491    pub h: Vec<u8>,
492    pub q: u64,
493    pub c: u32,
494    pub a: bool,
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub e: Option<String>,
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize)]
500pub struct DataChunk {
501    #[serde(with = "serde_bytes")]
502    pub h: Vec<u8>,
503    pub q: u64,
504    pub c: u32,
505    pub n: u32,
506    pub p: u64,
507    #[serde(with = "serde_bytes")]
508    pub d: Vec<u8>,
509}
510
511#[derive(Debug, Clone)]
512pub enum DataMessage {
513    Request(DataRequest),
514    Response(DataResponse),
515    QuoteRequest(DataQuoteRequest),
516    QuoteResponse(DataQuoteResponse),
517    Payment(DataPayment),
518    PaymentAck(DataPaymentAck),
519    Chunk(DataChunk),
520}
521
522fn default_htl() -> u8 {
523    BLOB_REQUEST_POLICY.max_htl
524}
525
526fn is_max_htl(htl: &u8) -> bool {
527    *htl == BLOB_REQUEST_POLICY.max_htl
528}
529
530/// Encode a request to wire format: [0x00][msgpack body]
531/// Uses named fields for cross-language compatibility with TypeScript
532pub fn encode_request(req: &DataRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
533    let body = rmp_serde::to_vec_named(req)?;
534    let mut result = Vec::with_capacity(1 + body.len());
535    result.push(MSG_TYPE_REQUEST);
536    result.extend(body);
537    Ok(result)
538}
539
540/// Encode a response to wire format: [0x01][msgpack body]
541/// Uses named fields for cross-language compatibility with TypeScript
542pub fn encode_response(res: &DataResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
543    let body = rmp_serde::to_vec_named(res)?;
544    let mut result = Vec::with_capacity(1 + body.len());
545    result.push(MSG_TYPE_RESPONSE);
546    result.extend(body);
547    Ok(result)
548}
549
550/// Encode a quote request to wire format: [0x02][msgpack body]
551pub fn encode_quote_request(req: &DataQuoteRequest) -> Result<Vec<u8>, rmp_serde::encode::Error> {
552    let body = rmp_serde::to_vec_named(req)?;
553    let mut result = Vec::with_capacity(1 + body.len());
554    result.push(MSG_TYPE_QUOTE_REQUEST);
555    result.extend(body);
556    Ok(result)
557}
558
559/// Encode a quote response to wire format: [0x03][msgpack body]
560pub fn encode_quote_response(res: &DataQuoteResponse) -> Result<Vec<u8>, rmp_serde::encode::Error> {
561    let body = rmp_serde::to_vec_named(res)?;
562    let mut result = Vec::with_capacity(1 + body.len());
563    result.push(MSG_TYPE_QUOTE_RESPONSE);
564    result.extend(body);
565    Ok(result)
566}
567
568pub fn encode_payment(req: &DataPayment) -> Result<Vec<u8>, rmp_serde::encode::Error> {
569    let body = rmp_serde::to_vec_named(req)?;
570    let mut result = Vec::with_capacity(1 + body.len());
571    result.push(MSG_TYPE_PAYMENT);
572    result.extend(body);
573    Ok(result)
574}
575
576pub fn encode_payment_ack(res: &DataPaymentAck) -> Result<Vec<u8>, rmp_serde::encode::Error> {
577    let body = rmp_serde::to_vec_named(res)?;
578    let mut result = Vec::with_capacity(1 + body.len());
579    result.push(MSG_TYPE_PAYMENT_ACK);
580    result.extend(body);
581    Ok(result)
582}
583
584pub fn encode_chunk(chunk: &DataChunk) -> Result<Vec<u8>, rmp_serde::encode::Error> {
585    let body = rmp_serde::to_vec_named(chunk)?;
586    let mut result = Vec::with_capacity(1 + body.len());
587    result.push(MSG_TYPE_CHUNK);
588    result.extend(body);
589    Ok(result)
590}
591
592/// Parse a wire format message
593pub fn parse_message(data: &[u8]) -> Result<DataMessage, rmp_serde::decode::Error> {
594    if data.is_empty() {
595        return Err(rmp_serde::decode::Error::LengthMismatch(0));
596    }
597
598    let msg_type = data[0];
599    let body = &data[1..];
600
601    match msg_type {
602        MSG_TYPE_REQUEST => {
603            let req: DataRequest = rmp_serde::from_slice(body)?;
604            Ok(DataMessage::Request(req))
605        }
606        MSG_TYPE_RESPONSE => {
607            let res: DataResponse = rmp_serde::from_slice(body)?;
608            Ok(DataMessage::Response(res))
609        }
610        MSG_TYPE_QUOTE_REQUEST => {
611            let req: DataQuoteRequest = rmp_serde::from_slice(body)?;
612            Ok(DataMessage::QuoteRequest(req))
613        }
614        MSG_TYPE_QUOTE_RESPONSE => {
615            let res: DataQuoteResponse = rmp_serde::from_slice(body)?;
616            Ok(DataMessage::QuoteResponse(res))
617        }
618        MSG_TYPE_PAYMENT => {
619            let req: DataPayment = rmp_serde::from_slice(body)?;
620            Ok(DataMessage::Payment(req))
621        }
622        MSG_TYPE_PAYMENT_ACK => {
623            let res: DataPaymentAck = rmp_serde::from_slice(body)?;
624            Ok(DataMessage::PaymentAck(res))
625        }
626        MSG_TYPE_CHUNK => {
627            let chunk: DataChunk = rmp_serde::from_slice(body)?;
628            Ok(DataMessage::Chunk(chunk))
629        }
630        _ => Err(rmp_serde::decode::Error::LengthMismatch(msg_type as u32)),
631    }
632}
633
634/// Convert hash to hex string for logging/map keys
635pub fn hash_to_hex(hash: &[u8]) -> String {
636    hash.iter().map(|b| format!("{:02x}", b)).collect()
637}
638
639/// Encode a DataMessage to wire format (deprecated - use encode_request/encode_response)
640pub fn encode_message(msg: &DataMessage) -> Result<Vec<u8>, rmp_serde::encode::Error> {
641    match msg {
642        DataMessage::Request(req) => encode_request(req),
643        DataMessage::Response(res) => encode_response(res),
644        DataMessage::QuoteRequest(req) => encode_quote_request(req),
645        DataMessage::QuoteResponse(res) => encode_quote_response(res),
646        DataMessage::Payment(req) => encode_payment(req),
647        DataMessage::PaymentAck(res) => encode_payment_ack(res),
648        DataMessage::Chunk(chunk) => encode_chunk(chunk),
649    }
650}