dittolive_ditto/transport/v3/
mod.rs

1use std::cmp::Ordering;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5
6use crate::presence::ConnectionType;
7#[cfg(doc)]
8use crate::transport::Presence;
9
10// NOTE(nimo): mod needed because `SiteId` is deprecated and gets used in serde derived impls
11#[allow(deprecated)]
12mod ditto_address {
13    use serde::{Deserialize, Serialize};
14
15    #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
16    /// DittoPeer identifier
17    #[serde(rename_all = "camelCase")]
18    pub struct DittoAddress {
19        pub(crate) pubkey: Vec<u8>,
20        /// SiteId is deprecated, only included here to prevent serialization faults
21        #[deprecated]
22        #[doc(hidden)]
23        #[serde(default)]
24        pub(crate) site_id: Option<u64>,
25    }
26}
27
28/// Graph of all known peers generated by [`Presence`] methods.
29#[derive(Debug, Clone, Deserialize, PartialEq)]
30#[serde(rename_all = "camelCase")]
31#[non_exhaustive]
32pub struct PresenceGraph {
33    /// The local peer.
34    pub local_peer: Peer,
35    /// Note that the peers in this set might not be directly connected to
36    /// the local peer. Some peers might be be connected indirectly via a
37    /// another peer or even form part of an isolated graph.
38    ///
39    /// Others yet might be discovered but disconnected due to version
40    /// incompatibilities.
41    pub remote_peers: Vec<Peer>,
42}
43
44/// Peer in the Ditto mesh.
45#[derive(Clone, Deserialize, PartialEq)]
46#[serde(rename_all = "camelCase")]
47#[non_exhaustive]
48pub struct Peer {
49    /// An address uniquely representing another device on the Ditto network.
50    pub address: ditto_address::DittoAddress,
51
52    /// The peer key is a unique identifier for a given peer, equal to or
53    /// derived from the cryptographic public key used to authenticate it.
54    ///
55    /// NOTE: This will be empty when a peer is not updated to the latest
56    /// version of the SDK.
57    #[serde(rename = "peerKeyString")]
58    pub peer_key: String,
59
60    /// The human-readable device name for a peer. This defaults to the
61    /// the hostname but can be manually set by the application developer.
62    pub device_name: String,
63
64    /// The operating system of a peer (if known).
65    pub os: Option<PresenceOs>,
66
67    /// Flag which indicates if this peer is connected to HyDRA. This is
68    /// represented as a simple flag since attempting add HyDRA as a node
69    /// to a graph would be extremely convoluted. The presence viewer
70    /// depicts the HyDRA connection with a simple cloud icon above a peer.
71    #[serde(alias = "isConnectedToDittoCloud")]
72    pub is_connected_to_ditto_server: bool,
73
74    /// A simplified boolean flag indicating whether the is peer is
75    /// compatible with our own peer (if known). Note that there _might_
76    /// be connections to this peer even if incompatible with our own
77    /// peer, provided that some other peers are able to interoperate.
78    pub is_compatible: Option<bool>,
79
80    /// The marketing version of the SDK (if known). For instance: `"1.0.3"`.
81    pub ditto_sdk_version: Option<String>,
82
83    /// List of current connections between this peer and other peers.
84    pub connections: Vec<Connection>,
85
86    /// Metadata associated with the peer, empty dictionary by default.
87    ///
88    /// Use [`Presence::set_peer_metadata()`] or
89    /// [`Presence::set_peer_metadata_json_str()`] to set this value.
90    /// Peer metadata is dynamic and may change over the lifecycle of the
91    /// [`PresenceGraph`]. It may be empty when a remote peer initially
92    /// appears in the presence graph and will be updated once the peer has
93    /// synced its metadata with the local peer.
94    ///
95    /// - See also: [`Presence::peer_metadata`] for details on usage of metadata.
96    pub peer_metadata: JsonValue,
97
98    /// Metadata associated with the peer by the identity service.
99    ///
100    /// Use an authentication webhook to set this value. See Ditto's online
101    /// documentation for more information on how to configure an authentication
102    /// webhook.
103    pub identity_service_metadata: JsonValue,
104}
105
106impl ::core::fmt::Debug for Peer {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.debug_struct("Peer")
109            .field("address", &self.address)
110            .field("peer_key", &self.peer_key)
111            .field("device_name", &self.device_name)
112            .field("os", &self.os)
113            .field(
114                "is_connected_to_ditto_server",
115                &self.is_connected_to_ditto_server,
116            )
117            .field("is_compatible", &self.is_compatible)
118            .field("ditto_sdk_version", &self.ditto_sdk_version)
119            .field("connections", &self.connections)
120            .field("peer_metadata", &self.peer_metadata)
121            .field("identity_service_metadata", &self.identity_service_metadata)
122            .finish_non_exhaustive()
123    }
124}
125
126/// Represents a connection between two peers in a Ditto mesh network.
127#[derive(Clone, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129#[non_exhaustive]
130pub struct Connection {
131    /// An string ID in the form `"from<->to:connection_type"`. These ids
132    /// are stable: the lower site Id will always be placed first.
133    ///
134    /// ## Example
135    /// "1<->2:Bluetooth"
136    pub id: String,
137
138    /// The peer key of the peer at one end of the connection.
139    ///
140    /// The assignment to `peer1` and `peer2` is deterministic and stable for
141    /// any two peers.
142    #[serde(rename = "peerKeyString1")]
143    pub peer1: String,
144
145    /// The peer key of the peer at the other end of the connection.
146    ///
147    /// The assignment to `peer1` and `peer2` is deterministic and stable for
148    /// any two peers.
149    #[serde(rename = "peerKeyString2")]
150    pub peer2: String,
151
152    /// The type of connection. One `Connection` will exist
153    /// for each connection type that might be active.
154    pub connection_type: ConnectionType,
155}
156
157impl ::core::fmt::Debug for Connection {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        f.debug_struct("Connection")
160            .field("id", &self.id)
161            .field("peer1", &self.peer1)
162            .field("peer2", &self.peer2)
163            .field("connection_type", &self.connection_type)
164            .finish_non_exhaustive()
165    }
166}
167
168impl Eq for Connection {}
169impl PartialEq<Self> for Connection {
170    fn eq(&self, other: &Self) -> bool {
171        self.id == other.id
172    }
173}
174
175impl PartialOrd for Connection {
176    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
177        Some(self.cmp(other))
178    }
179}
180
181impl Ord for Connection {
182    fn cmp(&self, other: &Self) -> Ordering {
183        self.id.cmp(&other.id)
184    }
185}
186
187/// Operating system used by a given peer.
188#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
189pub enum PresenceOs {
190    /// Peer is a generic device with unknown OS.
191    #[serde(rename = "Generic")]
192    Generic,
193
194    /// Peer is an Apple mobile device running iOS.
195    #[serde(rename = "iOS")]
196    Ios,
197
198    /// Peer is an Android mobile device.
199    #[serde(rename = "Android")]
200    Android,
201
202    /// Peer is a Linux device.
203    #[serde(rename = "Linux")]
204    Linux,
205
206    /// Peer is a Windows device.
207    #[serde(rename = "Windows")]
208    Windows,
209
210    /// Peer is an Apple device running MacOS.
211    #[serde(rename = "macOS")]
212    MacOS,
213}
214
215impl std::fmt::Display for PresenceOs {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            PresenceOs::Generic => write!(f, "Generic"),
219            PresenceOs::Ios => write!(f, "iOS"),
220            PresenceOs::Android => write!(f, "Android"),
221            PresenceOs::Linux => write!(f, "Linux"),
222            PresenceOs::MacOS => write!(f, "macOS"),
223            PresenceOs::Windows => write!(f, "Windows"),
224        }
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    const V3_PEER_JSON: &str = r#"
233            {
234                "localPeer": {
235                    "address": {"pubkey":[1]},
236                    "deviceName": "local-peer",
237                    "peerKeyString": "pkAQ",
238                    "peerMetadata": {},
239                    "identityServiceMetadata": {},
240                    "os": "macOS",
241                    "isConnectedToDittoCloud": false,
242                    "isCompatible": true,
243                    "dittoSdkVersion": "1.0.0",
244                    "connections" : [
245                        {
246                            "id": "pkAQ<->pkAg:Bluetooth",
247                            "peerKeyString1": "pkAQ",
248                            "peerKeyString2": "pkAg",
249                            "connectionType": "Bluetooth"
250                        },
251                        {
252                            "id": "pkAQ<->pkAw:AccessPoint",
253                            "peerKeyString1": "pkAQ",
254                            "peerKeyString2": "pkAw",
255                            "connectionType": "AccessPoint"
256                        },
257                        {
258                            "id": "pkAQ<->pkBA:WebSocket",
259                            "peerKeyString1": "pkAQ",
260                            "peerKeyString2": "pkBA",
261                            "connectionType": "WebSocket"
262                        }
263                    ]
264                },
265                "remotePeers": [
266                    {
267                        "address": {"pubkey":[2]},
268                        "peerKeyString": "pkAG",
269                        "peerMetadata": {},
270                        "identityServiceMetadata": {},
271                        "deviceName": "device-2",
272                        "os": "iOS",
273                        "isConnectedToDittoCloud": false,
274                        "isCompatible": true,
275                        "dittoSdkVersion": null,
276                        "connections" : [
277                            {
278                                "id": "pkAQ<->pkAg:Bluetooth",
279                                "peerKeyString1": "pkAQ",
280                                "peerKeyString2": "pkAg",
281                                "connectionType": "Bluetooth"
282                            }
283                        ]
284                    },
285                    {
286                        "address": {"pubkey":[3]},
287                        "peerKeyString": "pkAw",
288                        "peerMetadata": {},
289                        "identityServiceMetadata": {},
290                        "deviceName": "device-3",
291                        "os": "Android",
292                        "isConnectedToDittoCloud": false,
293                        "isCompatible": true,
294                        "dittoSdkVersion": "1.0.3",
295                        "connections" : [
296                            {
297                                "id": "pkAQ<->pkAw:AccessPoint",
298                                "peerKeyString1": "pkAQ",
299                                "peerKeyString2": "pkAw",
300                                "connectionType": "AccessPoint"
301                            }
302                        ]
303                    },
304                    {
305                        "address": {"pubkey":[4]},
306                        "peerKeyString": "pkBA",
307                        "peerMetadata": {},
308                        "identityServiceMetadata": {},
309                        "deviceName": "device-4",
310                        "os": "Linux",
311                        "isConnectedToDittoCloud": true,
312                        "isCompatible": true,
313                        "dittoSdkVersion": null,
314                        "connections" : [
315                            {
316                                "id": "pkAQ<->pkBA:WebSocket",
317                                "peerKeyString1": "pkAQ",
318                                "peerKeyString2": "pkBA",
319                                "connectionType": "WebSocket"
320                            }
321                        ]
322                    }
323                ]
324            }
325        "#;
326
327    #[test]
328    fn test_json_parsing() {
329        #![allow(deprecated)]
330
331        let graph: PresenceGraph = serde_json::from_str(V3_PEER_JSON).unwrap();
332
333        // Local Peer
334        assert_eq!(graph.local_peer.address.pubkey, vec![1]);
335        assert_eq!(graph.local_peer.device_name, "local-peer");
336        assert_eq!(graph.local_peer.os, Some(PresenceOs::MacOS));
337        assert!(!graph.local_peer.is_connected_to_ditto_server);
338        assert_eq!(graph.local_peer.connections.len(), 3);
339        assert_eq!(graph.local_peer.peer_key, "pkAQ");
340        assert_eq!(graph.local_peer.connections[0].peer1, "pkAQ");
341        assert_eq!(graph.local_peer.connections[0].peer2, "pkAg");
342        assert_eq!(graph.local_peer.connections[1].peer1, "pkAQ");
343        assert_eq!(graph.local_peer.connections[1].peer2, "pkAw");
344        assert_eq!(graph.local_peer.connections[2].peer1, "pkAQ");
345        assert_eq!(graph.local_peer.connections[2].peer2, "pkBA");
346
347        // Remote Peers
348
349        assert_eq!(graph.remote_peers[0].address.pubkey, vec![2]);
350        assert_eq!(graph.remote_peers[0].device_name, "device-2");
351        assert_eq!(graph.remote_peers[0].os, Some(PresenceOs::Ios));
352        assert!(!graph.remote_peers[0].is_connected_to_ditto_server);
353        let connections = &graph.remote_peers[0].connections;
354        assert_eq!(connections[0].peer1, "pkAQ");
355        assert_eq!(connections[0].peer2, "pkAg");
356
357        assert_eq!(graph.remote_peers[1].address.pubkey, vec![3]);
358        assert_eq!(graph.remote_peers[1].device_name, "device-3");
359        assert_eq!(graph.remote_peers[1].os, Some(PresenceOs::Android));
360        assert!(!graph.remote_peers[1].is_connected_to_ditto_server);
361        let connections = &graph.remote_peers[1].connections;
362        assert_eq!(connections[0].peer1, "pkAQ");
363        assert_eq!(connections[0].peer2, "pkAw");
364
365        assert_eq!(graph.remote_peers[2].address.pubkey, vec![4]);
366        assert_eq!(graph.remote_peers[2].device_name, "device-4");
367        assert_eq!(graph.remote_peers[2].os, Some(PresenceOs::Linux));
368        assert!(graph.remote_peers[2].is_connected_to_ditto_server);
369        let connections = &graph.remote_peers[2].connections;
370        assert_eq!(connections[0].peer1, "pkAQ");
371        assert_eq!(connections[0].peer2, "pkBA");
372    }
373}