dittolive_ditto/transport/v3/
mod.rs

1use std::cmp::Ordering;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value as JsonValue;
5
6#[cfg(doc)]
7use crate::transport::Presence;
8
9// NOTE(nimo): mod needed because `SiteId` is deprecated and gets used in serde derived impls
10#[allow(deprecated)]
11mod ditto_address {
12    use serde::{Deserialize, Serialize};
13
14    #[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
15    /// DittoPeer identifier
16    #[serde(rename_all = "camelCase")]
17    pub struct DittoAddress {
18        pub(crate) pubkey: Vec<u8>,
19        pub(crate) site_id: Option<crate::transport::SiteId>,
20    }
21}
22pub use self::ditto_address::DittoAddress;
23
24/// Graph of all known peers generated by [`Presence`] methods.
25#[derive(Debug, Clone, Deserialize, PartialEq)]
26#[serde(rename_all = "camelCase")]
27#[non_exhaustive]
28pub struct PresenceGraph {
29    /// The local peer.
30    pub local_peer: Peer,
31    /// Note that the peers in this set might not be directly connected to
32    /// the local peer. Some peers might be be connected indirectly via a
33    /// another peer or even form part of an isolated graph.
34    ///
35    /// Others yet might be discovered but disconnected due to version
36    /// incompatibilities.
37    pub remote_peers: Vec<Peer>,
38}
39
40/// Peer in the Ditto mesh.
41#[derive(Clone, Deserialize, PartialEq)]
42#[serde(rename_all = "camelCase")]
43#[non_exhaustive]
44pub struct Peer {
45    /// An address uniquely representing another device on the Ditto network.
46    pub address: DittoAddress,
47
48    #[doc(hidden)]
49    #[deprecated(note = "use `.peer_key_string` instead")]
50    pub peer_key: Vec<u8>,
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    pub peer_key_string: String,
58
59    /// The human-readable device name for a peer. This defaults to the
60    /// the hostname but can be manually set by the application developer.
61    pub device_name: String,
62
63    /// The operating system of a peer (if known).
64    pub os: Option<PresenceOs>,
65
66    /// This field is no longer in use and will always be `0`.
67    #[doc(hidden)]
68    #[deprecated(
69        note = "This feature has been removed, this value will always be 0 from SDK release 4.8.0"
70    )]
71    #[serde(default, skip_deserializing)]
72    pub query_overlap_group: u8,
73
74    /// Flag which indicates if this peer is connected to HyDRA. This is
75    /// represented as a simple flag since attempting add HyDRA as a node
76    /// to a graph would be extremely convoluted. The presence viewer
77    /// depicts the HyDRA connection with a simple cloud icon above a peer.
78    pub is_connected_to_ditto_cloud: bool,
79
80    /// A simplified boolean flag indicating whether the is peer is
81    /// compatible with our own peer (if known). Note that there _might_
82    /// be connections to this peer even if incompatible with our own
83    /// peer, provided that some other peers are able to interoperate.
84    pub is_compatible: Option<bool>,
85
86    /// The marketing version of the SDK (if known). For instance: `"1.0.3"`.
87    pub ditto_sdk_version: Option<String>,
88
89    /// List of current connections between this peer and other peers.
90    pub connections: Vec<Connection>,
91
92    /// Metadata associated with the peer, empty dictionary by default.
93    ///
94    /// Use [`Presence::set_peer_metadata()`] or
95    /// [`Presence::set_peer_metadata_json_str()`] to set this value.
96    /// Peer metadata is dynamic and may change over the lifecycle of the
97    /// [`PresenceGraph`]. It may be empty when a remote peer initially
98    /// appears in the presence graph and will be updated once the peer has
99    /// synced its metadata with the local peer.
100    ///
101    /// - See also: [`Presence::peer_metadata`] for details on usage of metadata.
102    pub peer_metadata: JsonValue,
103
104    /// Metadata associated with the peer by the identity service.
105    ///
106    /// Use an authentication webhook to set this value. See Ditto's online
107    /// documentation for more information on how to configure an authentication
108    /// webhook.
109    pub identity_service_metadata: JsonValue,
110}
111
112impl ::core::fmt::Debug for Peer {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        f.debug_struct("Peer")
115            .field("address", &self.address)
116            .field("peer_key_string", &self.peer_key_string)
117            .field("device_name", &self.device_name)
118            .field("os", &self.os)
119            .field(
120                "is_connected_to_ditto_cloud",
121                &self.is_connected_to_ditto_cloud,
122            )
123            .field("is_compatible", &self.is_compatible)
124            .field("ditto_sdk_version", &self.ditto_sdk_version)
125            .field("connections", &self.connections)
126            .field("peer_metadata", &self.peer_metadata)
127            .field("identity_service_metadata", &self.identity_service_metadata)
128            .finish_non_exhaustive()
129    }
130}
131
132// Re-exporting using the old name for this type, `UndirectedConnection`
133//
134// Technically this shouldn't be strictly necessary because `UndirectedConnection`
135// was actually unnameable from outside the crate, so no client code should
136// have been able to construct or destructure or even import this type by its
137// old name anyway.
138#[doc(hidden)]
139#[allow(unused_imports)]
140#[deprecated(note = "Use `Connection` instead")]
141pub use Connection as UndirectedConnection;
142
143/// Represents a connection between two peers in a Ditto mesh network.
144#[derive(Clone, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146#[non_exhaustive]
147pub struct Connection {
148    /// An string ID in the form `"from<->to:connection_type"`. These ids
149    /// are stable: the lower site Id will always be placed first.
150    ///
151    /// ## Example
152    /// "1<->2:Bluetooth"
153    pub id: String,
154
155    /// The peer with the lower Peer Key.
156    #[doc(hidden)]
157    #[deprecated(note = "use `.peer_key_string1` instead")]
158    pub peer1: Vec<u8>,
159
160    /// The peer with the higher Peer Key.
161    #[doc(hidden)]
162    #[deprecated(note = "use `.peer_key_string2` instead")]
163    pub peer2: Vec<u8>,
164
165    /// The peer key of the peer at one end of the connection, as a String
166    ///
167    /// The assignment to `peer_key_string1` and `peer_key_string2` is deterministic
168    /// and stable for any two peers.
169    pub peer_key_string1: String,
170
171    /// The peer key of the peer at the other end of the connection, as a String
172    ///
173    /// The assignment to `peer_key_string1` and `peer_key_string2` is deterministic
174    /// and stable for any two peers.
175    pub peer_key_string2: String,
176
177    /// The type of connection. One `Connection` will exist
178    /// for each connection type that might be active.
179    pub connection_type: super::ConnectionType,
180
181    /// A made-up figure that changes based on RSSI. This may become meaningful
182    /// if more accurate range-finding transports are added in future.
183    pub approximate_distance_in_meters: Option<f32>,
184}
185
186impl ::core::fmt::Debug for Connection {
187    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
188        f.debug_struct("Connection")
189            .field("id", &self.id)
190            .field("peer_key_string1", &self.peer_key_string1)
191            .field("peer_key_string2", &self.peer_key_string2)
192            .field("connection_type", &self.connection_type)
193            .field(
194                "approximate_distance_in_meters",
195                &self.approximate_distance_in_meters,
196            )
197            .finish_non_exhaustive()
198    }
199}
200
201impl Eq for Connection {}
202impl PartialEq<Self> for Connection {
203    fn eq(&self, other: &Self) -> bool {
204        self.id == other.id
205    }
206}
207
208impl PartialOrd for Connection {
209    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
210        Some(self.cmp(other))
211    }
212}
213
214impl Ord for Connection {
215    fn cmp(&self, other: &Self) -> Ordering {
216        self.id.cmp(&other.id)
217    }
218}
219
220/// Operating system used by a given peer.
221#[derive(PartialEq, Eq, Hash, Clone, Debug, Serialize, Deserialize)]
222pub enum PresenceOs {
223    /// Peer is a generic device with unknown OS.
224    #[serde(rename = "Generic")]
225    Generic,
226
227    /// Peer is an Apple mobile device running iOS.
228    #[serde(rename = "iOS")]
229    Ios,
230
231    /// Peer is an Android mobile device.
232    #[serde(rename = "Android")]
233    Android,
234
235    /// Peer is a Linux device.
236    #[serde(rename = "Linux")]
237    Linux,
238
239    /// Peer is a Windows device.
240    #[serde(rename = "Windows")]
241    Windows,
242
243    /// Peer is an Apple device running MacOS.
244    #[serde(rename = "macOS")]
245    MacOS,
246}
247
248impl std::fmt::Display for PresenceOs {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        match self {
251            PresenceOs::Generic => write!(f, "Generic"),
252            PresenceOs::Ios => write!(f, "iOS"),
253            PresenceOs::Android => write!(f, "Android"),
254            PresenceOs::Linux => write!(f, "Linux"),
255            PresenceOs::MacOS => write!(f, "macOS"),
256            PresenceOs::Windows => write!(f, "Windows"),
257        }
258    }
259}
260
261// NOTE(nick): There are a few weird things going on here, allow me to explain:
262//
263// - Circa June 2024 we are deprecating this type in favor of [`super::ConnectionType`].
264//
265// - The deprecation note says "Use `ConnectionType` instead", because this type is only visible to
266//   the outside via `dittolive_ditto::transport::V3ConnectionType` due to a re-export in the
267//   transport module. Therefore the deprecation note is telling users to move from
268//   `V3ConnectionType` to just `ConnectionType` in the same module
269//
270// - When adding the `#[deprecated]` tag, the derived impls of `Serialize` and `Deserialize` for
271//   `ConnectionType` caused warnings (aka CI failures) because those impls referred to the type and
272//   the fields, triggering the deprecation warning. Adding an `#[allow(deprecated)]` to the type
273//   itself did not resolve the warnings for the derived impls.
274//
275// - To work around this, we put the type into its own module and used `#[allow(deprecated)]` on the
276//   whole module, which encompasses all of the derived impls as well. Then we needed a simple
277//   re-export which also needed `#[allow(deprecated)]`
278#[allow(deprecated)]
279pub use connection_type::ConnectionType;
280#[allow(deprecated)]
281mod connection_type {
282    use super::*;
283
284    /// Defines a simplified connection type between peers for reporting presence
285    /// info.
286    ///
287    /// These connections indicate P2P connections _only_. A connection to HyDRA
288    /// is recorded by a simple boolean flag on the peer.
289    #[doc(hidden)]
290    #[deprecated(note = "Use `ConnectionType` instead")]
291    #[derive(PartialEq, Eq, Hash, Ord, PartialOrd, Clone, Copy, Debug, Serialize, Deserialize)]
292    pub enum ConnectionType {
293        Bluetooth,
294        AccessPoint,
295        P2PWiFi,
296        WebSocket,
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use super::*;
303
304    const V3_PEER_JSON: &str = r#"
305            {
306                "localPeer": {
307                    "address": {"siteId":1, "pubkey":[1]},
308                    "deviceName": "local-peer",
309                    "peerKey": [1],
310                    "peerKeyString": "pkAQ",
311                    "peerMetadata": {},
312                    "identityServiceMetadata": {},
313                    "os": "macOS",
314                    "isConnectedToDittoCloud": false,
315                    "isCompatible": true,
316                    "dittoSdkVersion": "1.0.0",
317                    "meshRole": 0,
318                    "queryOverlapGroup": 0,
319                    "connections" : [
320                        {
321                            "id": "pkAQ<->pkAg:Bluetooth",
322                            "peer1": [1],
323                            "peer2": [2],
324                            "peerKeyString1": "pkAQ",
325                            "peerKeyString2": "pkAg",
326                            "connectionType": "Bluetooth",
327                            "approximateDistanceInMeters": null
328                        },
329                        {
330                            "id": "pkAQ<->pkAw:AccessPoint",
331                            "peer1": [1],
332                            "peer2": [3],
333                            "peerKeyString1": "pkAQ",
334                            "peerKeyString2": "pkAw",
335                            "connectionType": "AccessPoint",
336                            "approximateDistanceInMeters": null
337                        },
338                        {
339                            "id": "pkAQ<->pkBA:WebSocket",
340                            "peer1": [1],
341                            "peer2": [4],
342                            "peerKeyString1": "pkAQ",
343                            "peerKeyString2": "pkBA",
344                            "connectionType": "WebSocket",
345                            "approximateDistanceInMeters": null
346                        }
347                    ]
348                },
349                "remotePeers": [
350                    {
351                        "address": {"siteId":2, "pubkey":[2]},
352                        "peerKey": [2],
353                        "peerKeyString": "pkAG",
354                        "peerMetadata": {},
355                        "identityServiceMetadata": {},
356                        "deviceName": "device-2",
357                        "os": "iOS",
358                        "isConnectedToDittoCloud": false,
359                        "isCompatible": true,
360                        "dittoSdkVersion": null,
361                        "meshRole": 0,
362                        "queryOverlapGroup": 0,
363                        "connections" : [
364                            {
365                                "id": "pkAQ<->pkAg:Bluetooth",
366                                "peer1": [1],
367                                "peer2": [2],
368                                "peerKeyString1": "pkAQ",
369                                "peerKeyString2": "pkAg",
370                                "connectionType": "Bluetooth",
371                                "approximateDistanceInMeters": null
372                            }
373                        ]
374                    },
375                    {
376                        "address": {"siteId":3, "pubkey":[3]},
377                        "peerKey": [3],
378                        "peerKeyString": "pkAw",
379                        "peerMetadata": {},
380                        "identityServiceMetadata": {},
381                        "deviceName": "device-3",
382                        "os": "Android",
383                        "isConnectedToDittoCloud": false,
384                        "isCompatible": true,
385                        "dittoSdkVersion": "1.0.3",
386                        "meshRole": 32,
387                        "queryOverlapGroup": 32,
388                        "connections" : [
389                            {
390                                "id": "pkAQ<->pkAw:AccessPoint",
391                                "peer1": [1],
392                                "peer2": [3],
393                                "peerKeyString1": "pkAQ",
394                                "peerKeyString2": "pkAw",
395                                "connectionType": "AccessPoint",
396                                "approximateDistanceInMeters": null
397                            }
398                        ]
399                    },
400                    {
401                        "address": {"siteId":4, "pubkey":[4]},
402                        "peerKey": [4],
403                        "peerKeyString": "pkBA",
404                        "peerMetadata": {},
405                        "identityServiceMetadata": {},
406                        "deviceName": "device-4",
407                        "os": "Linux",
408                        "isConnectedToDittoCloud": false,
409                        "isCompatible": true,
410                        "dittoSdkVersion": null,
411                        "connections" : [
412                            {
413                                "id": "pkAQ<->pkBA:WebSocket",
414                                "peer1": [1],
415                                "peer2": [4],
416                                "peerKeyString1": "pkAQ",
417                                "peerKeyString2": "pkBA",
418                                "connectionType": "WebSocket",
419                                "approximateDistanceInMeters": null
420                            }
421                        ]
422                    }
423                ]
424            }
425        "#;
426
427    #[test]
428    fn test_json_parsing() {
429        #![allow(deprecated)]
430
431        let graph: PresenceGraph = serde_json::from_str(V3_PEER_JSON).unwrap();
432
433        // Local Peer
434        assert_eq!(graph.local_peer.address.pubkey, vec![1]);
435        assert_eq!(graph.local_peer.device_name, "local-peer");
436        assert_eq!(graph.local_peer.os, Some(PresenceOs::MacOS));
437        assert_eq!(graph.local_peer.query_overlap_group, 0);
438        assert_eq!(graph.local_peer.connections.len(), 3);
439        assert_eq!(graph.local_peer.peer_key, vec![1]);
440        assert_eq!(graph.local_peer.peer_key_string, "pkAQ");
441        assert_eq!(graph.local_peer.connections[0].peer_key_string1, "pkAQ");
442        assert_eq!(graph.local_peer.connections[0].peer_key_string2, "pkAg");
443        assert_eq!(graph.local_peer.connections[1].peer_key_string1, "pkAQ");
444        assert_eq!(graph.local_peer.connections[1].peer_key_string2, "pkAw");
445        assert_eq!(graph.local_peer.connections[2].peer_key_string1, "pkAQ");
446        assert_eq!(graph.local_peer.connections[2].peer_key_string2, "pkBA");
447
448        // Remote Peers
449
450        assert_eq!(graph.remote_peers[0].address.pubkey, vec![2]);
451        assert_eq!(graph.remote_peers[0].peer_key, vec![2]);
452        assert_eq!(graph.remote_peers[0].device_name, "device-2");
453        assert_eq!(graph.remote_peers[0].os, Some(PresenceOs::Ios));
454        let connections = &graph.remote_peers[0].connections;
455        assert_eq!(connections[0].peer_key_string1, "pkAQ");
456        assert_eq!(connections[0].peer_key_string2, "pkAg");
457
458        assert_eq!(graph.remote_peers[1].address.pubkey, vec![3]);
459        assert_eq!(graph.remote_peers[1].peer_key, vec![3]);
460        assert_eq!(graph.remote_peers[1].device_name, "device-3");
461        assert_eq!(graph.remote_peers[1].os, Some(PresenceOs::Android));
462        let connections = &graph.remote_peers[1].connections;
463        assert_eq!(connections[0].peer_key_string1, "pkAQ");
464        assert_eq!(connections[0].peer_key_string2, "pkAw");
465
466        assert_eq!(graph.remote_peers[2].address.pubkey, vec![4]);
467        assert_eq!(graph.remote_peers[2].peer_key, vec![4]);
468        assert_eq!(graph.remote_peers[2].device_name, "device-4");
469        assert_eq!(graph.remote_peers[2].os, Some(PresenceOs::Linux));
470        let connections = &graph.remote_peers[2].connections;
471        assert_eq!(connections[0].peer_key_string1, "pkAQ");
472        assert_eq!(connections[0].peer_key_string2, "pkBA");
473
474        #[allow(deprecated)]
475        {
476            assert_eq!(graph.local_peer.query_overlap_group, 0);
477            assert_eq!(graph.remote_peers[0].query_overlap_group, 0);
478            assert_eq!(graph.remote_peers[1].query_overlap_group, 0);
479            assert_eq!(graph.remote_peers[2].query_overlap_group, 0);
480        }
481    }
482}