Skip to main content

openrtc/
signaling.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use futures::stream::BoxStream;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "camelCase")]
8pub struct DeviceCapabilities {
9    pub can_host: bool,
10    pub can_sync: bool,
11    pub read_only: bool,
12}
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct Device {
17    pub app_tag: Option<String>,
18    pub device_id: String,
19    pub user_id: Option<String>,
20    pub device_name: String,
21    pub platform_type: Option<String>,
22    pub capabilities: Option<DeviceCapabilities>,
23    pub session_id: Option<String>,
24    pub node_id: Option<String>,
25    pub tag: Option<String>,
26    pub kind: Option<String>,
27    pub metadata: Option<String>,
28    pub online: bool,
29    pub ticket: Option<String>,
30    pub last_seen_at: Option<serde_json::Value>,
31    pub expires_at: Option<serde_json::Value>,
32    pub created_at: Option<serde_json::Value>,
33    pub updated_at: Option<serde_json::Value>,
34    /// Device IDs that this peer has explicitly excluded from auto-connect.
35    /// Set when the peer manually disconnects from a device; cleared on manual reconnect.
36    /// All other peers check this before dialing in.
37    #[serde(default)]
38    pub excluded_peers: Vec<String>,
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(rename_all = "camelCase")]
43pub struct E2eeInfo {
44    pub session_key_id: String,
45    pub cipher_text: String,
46    pub iv: String,
47    pub salt: String,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct SignalingSession {
53    pub connection_id: String,
54    pub initiator: String,
55    pub target: String,
56    pub initiator_device_id: String,
57    pub target_device_id: String,
58    pub connection_type: Option<String>,
59    pub offer: Option<serde_json::Value>,
60    pub offer_e2ee: Option<E2eeInfo>,
61    pub answer: Option<serde_json::Value>,
62    pub answer_e2ee: Option<E2eeInfo>,
63    #[serde(default)]
64    pub ice_candidates: Vec<serde_json::Value>,
65    pub initiator_node_id: Option<String>,
66    pub target_node_id: Option<String>,
67    pub initiator_endpoint_addr: Option<String>,
68    pub target_endpoint_addr: Option<String>,
69    pub intent: Option<String>,
70    pub app_tag: Option<String>,
71    pub created_at: Option<i64>,
72    pub expires_at: Option<i64>,
73    pub state: String,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[serde(tag = "type", rename_all = "camelCase")]
78pub enum SessionEvent {
79    Added {
80        session: SignalingSession,
81    },
82    Modified {
83        session: SignalingSession,
84    },
85    Removed {
86        #[serde(rename = "sessionId")]
87        session_id: String,
88    },
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "camelCase")]
93pub enum DeviceEvent {
94    Added {
95        device: Device,
96    },
97    Modified {
98        device: Device,
99    },
100    Removed {
101        #[serde(rename = "deviceId")]
102        device_id: String,
103    },
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub struct SignalingEnvelope {
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub app_tag: Option<String>,
111    pub sender_id: String,
112    pub target_id: String,
113    pub payload: String,
114    pub state: Option<String>,
115    pub reply_payload: Option<String>,
116    pub timestamp: i64,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub sender_user_id: Option<String>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub target_user_id: Option<String>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub expires_at: Option<i64>,
123}
124
125#[cfg(target_arch = "wasm32")]
126pub trait SendSyncBound {}
127#[cfg(target_arch = "wasm32")]
128impl<T> SendSyncBound for T {}
129
130#[cfg(not(target_arch = "wasm32"))]
131pub trait SendSyncBound: Send + Sync {}
132#[cfg(not(target_arch = "wasm32"))]
133impl<T: Send + Sync> SendSyncBound for T {}
134
135#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
136#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
137pub trait SignalingBackend: SendSyncBound {
138    async fn update_presence(
139        &self,
140        user_id: &str,
141        local_node_id: &str,
142        ticket_str: &str,
143        is_online: bool,
144        name: &str,
145        ttl_ms: u64,
146        metadata: Option<&str>,
147    ) -> Result<()>;
148
149    async fn set_offline(&self, user_id: &str, local_node_id: &str) -> Result<()>;
150
151    /// Publish hot liveness/ticket state for a device.
152    ///
153    /// Firestore `update_presence` is the durable roster record. Implementations
154    /// with a cheaper live-presence plane, such as RTDB, override this so native
155    /// clients do not need Firestore heartbeat writes.
156    async fn update_live_presence(
157        &self,
158        _user_id: &str,
159        _local_node_id: &str,
160        _ticket_str: &str,
161        _name: &str,
162        _metadata: Option<&str>,
163    ) -> Result<()> {
164        #[cfg(target_arch = "wasm32")]
165        return Ok(());
166
167        #[cfg(not(target_arch = "wasm32"))]
168        anyhow::bail!("native signaling backend does not implement RTDB live presence")
169    }
170
171    /// Mark the live-presence plane offline without requiring a Firestore
172    /// heartbeat write.
173    async fn set_live_presence_offline(&self, _user_id: &str, _local_node_id: &str) -> Result<()> {
174        #[cfg(target_arch = "wasm32")]
175        return Ok(());
176
177        #[cfg(not(target_arch = "wasm32"))]
178        anyhow::bail!("native signaling backend does not implement RTDB offline presence")
179    }
180
181    async fn update_device(
182        &self,
183        user_id: &str,
184        device_id: &str,
185        device_name: Option<&str>,
186        capabilities: Option<DeviceCapabilities>,
187        metadata: Option<&str>,
188    ) -> Result<()>;
189
190    async fn delete_device(&self, user_id: &str, device_id: &str) -> Result<()>;
191
192    /// Update the `excludedPeers` list on the local device's presence document.
193    /// Called on disconnect (add `remote_device_id`) and reconnect (remove it).
194    async fn set_excluded_peers(
195        &self,
196        user_id: &str,
197        local_node_id: &str,
198        excluded_peers: &[String],
199    ) -> Result<()>;
200
201    async fn search_devices(
202        &self,
203        user_id: &str,
204        exclude_node_id: Option<&str>,
205    ) -> Result<Vec<Device>>;
206
207    async fn list_devices(
208        &self,
209        user_id: &str,
210        exclude_node_id: Option<&str>,
211    ) -> Result<Vec<Device>>;
212
213    async fn send_message(
214        &self,
215        sender_id: &str,
216        target_id: &str,
217        payload: &str,
218        state: Option<&str>,
219        reply_payload: Option<&str>,
220    ) -> Result<String>;
221
222    async fn subscribe_devices(
223        &self,
224        user_id: &str,
225    ) -> Result<BoxStream<'static, Result<Vec<DeviceEvent>>>>;
226
227    async fn create_session(&self, session: SignalingSession) -> Result<()>;
228
229    async fn update_session(&self, session_id: &str, update_data: serde_json::Value) -> Result<()>;
230
231    async fn subscribe_sessions(
232        &self,
233        local_device_id: &str,
234    ) -> Result<BoxStream<'static, Result<Vec<SessionEvent>>>>;
235}