Skip to main content

ruma_federation_api/transactions/
edu.rs

1//! Edu type and variant content structs.
2
3use std::collections::BTreeMap;
4
5use js_int::UInt;
6use ruma_common::{
7    OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, OwnedUserId,
8    encryption::{CrossSigningKey, DeviceKeys},
9    presence::PresenceState,
10    serde::{Raw, from_raw_json_value},
11    to_device::DeviceIdOrAllDevices,
12};
13use ruma_events::{AnyToDeviceEventContent, ToDeviceEventType, receipt::Receipt};
14use serde::{Deserialize, Serialize, de};
15use serde_json::value::RawValue as RawJsonValue;
16
17/// Type for passing ephemeral data to homeservers.
18#[derive(Clone, Debug, Serialize)]
19#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
20#[serde(tag = "edu_type", content = "content")]
21pub enum Edu {
22    /// An EDU representing presence updates for users of the sending homeserver.
23    #[serde(rename = "m.presence")]
24    Presence(PresenceContent),
25
26    /// An EDU representing receipt updates for users of the sending homeserver.
27    #[serde(rename = "m.receipt")]
28    Receipt(ReceiptContent),
29
30    /// A typing notification EDU for a user in a room.
31    #[serde(rename = "m.typing")]
32    Typing(TypingContent),
33
34    /// An EDU that lets servers push details to each other when one of their users adds
35    /// a new device to their account, required for E2E encryption to correctly target the
36    /// current set of devices for a given user.
37    #[serde(rename = "m.device_list_update")]
38    DeviceListUpdate(DeviceListUpdateContent),
39
40    /// An EDU that lets servers push send events directly to a specific device on a
41    /// remote server - for instance, to maintain an Olm E2E encrypted message channel
42    /// between a local and remote device.
43    #[serde(rename = "m.direct_to_device")]
44    DirectToDevice(DirectDeviceContent),
45
46    /// An EDU that lets servers push details to each other when one of their users updates their
47    /// cross-signing keys.
48    #[serde(rename = "m.signing_key_update")]
49    SigningKeyUpdate(SigningKeyUpdateContent),
50
51    #[doc(hidden)]
52    #[serde(untagged)]
53    _Custom(CustomEdu),
54}
55
56impl<'de> Deserialize<'de> for Edu {
57    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58    where
59        D: de::Deserializer<'de>,
60    {
61        #[derive(Debug, Deserialize)]
62        struct EduDeHelper {
63            edu_type: String,
64            content: Box<RawJsonValue>,
65        }
66
67        let json = Box::<RawJsonValue>::deserialize(deserializer)?;
68        let EduDeHelper { edu_type, content } = from_raw_json_value(&json)?;
69
70        Ok(match edu_type.as_ref() {
71            "m.presence" => Self::Presence(from_raw_json_value(&content)?),
72            "m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
73            "m.typing" => Self::Typing(from_raw_json_value(&content)?),
74            "m.device_list_update" => Self::DeviceListUpdate(from_raw_json_value(&content)?),
75            "m.direct_to_device" => Self::DirectToDevice(from_raw_json_value(&content)?),
76            "m.signing_key_update" => Self::SigningKeyUpdate(from_raw_json_value(&content)?),
77            _ => Self::_Custom(CustomEdu { edu_type, content }),
78        })
79    }
80}
81
82/// The content for "m.presence" Edu.
83#[derive(Clone, Debug, Deserialize, Serialize)]
84#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
85pub struct PresenceContent {
86    /// A list of presence updates that the receiving server is likely to be interested in.
87    pub push: Vec<PresenceUpdate>,
88}
89
90impl PresenceContent {
91    /// Creates a new `PresenceContent`.
92    pub fn new(push: Vec<PresenceUpdate>) -> Self {
93        Self { push }
94    }
95}
96
97/// An update to the presence of a user.
98#[derive(Clone, Debug, Deserialize, Serialize)]
99#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
100pub struct PresenceUpdate {
101    /// The user ID this presence EDU is for.
102    pub user_id: OwnedUserId,
103
104    /// The presence of the user.
105    pub presence: PresenceState,
106
107    /// An optional description to accompany the presence.
108    #[serde(skip_serializing_if = "Option::is_none")]
109    pub status_msg: Option<String>,
110
111    /// The number of milliseconds that have elapsed since the user last did something.
112    pub last_active_ago: UInt,
113
114    /// Whether or not the user is currently active.
115    ///
116    /// Defaults to false.
117    #[serde(default)]
118    pub currently_active: bool,
119}
120
121impl PresenceUpdate {
122    /// Creates a new `PresenceUpdate` with the given `user_id`, `presence` and `last_activity`.
123    pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self {
124        Self {
125            user_id,
126            presence,
127            last_active_ago: last_activity,
128            status_msg: None,
129            currently_active: false,
130        }
131    }
132}
133
134/// The content for "m.receipt" Edu.
135#[derive(Clone, Debug, Deserialize, Serialize)]
136#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
137pub struct ReceiptContent {
138    /// Receipts for a particular room.
139    #[serde(flatten)]
140    pub receipts: BTreeMap<OwnedRoomId, ReceiptMap>,
141}
142
143impl ReceiptContent {
144    /// Creates a new `ReceiptContent`.
145    pub fn new(receipts: BTreeMap<OwnedRoomId, ReceiptMap>) -> Self {
146        Self { receipts }
147    }
148}
149
150/// Mapping between user and `ReceiptData`.
151#[derive(Clone, Debug, Deserialize, Serialize)]
152#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
153pub struct ReceiptMap {
154    /// Read receipts for users in the room.
155    #[serde(rename = "m.read")]
156    pub read: BTreeMap<OwnedUserId, ReceiptData>,
157}
158
159impl ReceiptMap {
160    /// Creates a new `ReceiptMap`.
161    pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
162        Self { read }
163    }
164}
165
166/// Metadata about the event that was last read and when.
167#[derive(Clone, Debug, Deserialize, Serialize)]
168#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
169pub struct ReceiptData {
170    /// Metadata for the read receipt.
171    pub data: Receipt,
172
173    /// The extremity event ID the user has read up to.
174    pub event_ids: Vec<OwnedEventId>,
175}
176
177impl ReceiptData {
178    /// Creates a new `ReceiptData`.
179    pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
180        Self { data, event_ids }
181    }
182}
183
184/// The content for "m.typing" Edu.
185#[derive(Clone, Debug, Deserialize, Serialize)]
186#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
187pub struct TypingContent {
188    /// The room where the user's typing status has been updated.
189    pub room_id: OwnedRoomId,
190
191    /// The user ID that has had their typing status changed.
192    pub user_id: OwnedUserId,
193
194    /// Whether the user is typing in the room or not.
195    pub typing: bool,
196}
197
198impl TypingContent {
199    /// Creates a new `TypingContent`.
200    pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self {
201        Self { room_id, user_id, typing }
202    }
203}
204
205/// The description of the direct-to- device message.
206#[derive(Clone, Debug, Deserialize, Serialize)]
207#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
208pub struct DeviceListUpdateContent {
209    /// The user ID who owns the device.
210    pub user_id: OwnedUserId,
211
212    /// The ID of the device whose details are changing.
213    pub device_id: OwnedDeviceId,
214
215    /// The public human-readable name of this device.
216    ///
217    /// Will be absent if the device has no name.
218    #[serde(skip_serializing_if = "Option::is_none")]
219    pub device_display_name: Option<String>,
220
221    /// An ID sent by the server for this update, unique for a given user_id.
222    pub stream_id: UInt,
223
224    /// The stream_ids of any prior m.device_list_update EDUs sent for this user which have not
225    /// been referred to already in an EDU's prev_id field.
226    #[serde(default, skip_serializing_if = "Vec::is_empty")]
227    pub prev_id: Vec<UInt>,
228
229    /// True if the server is announcing that this device has been deleted.
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub deleted: Option<bool>,
232
233    /// The updated identity keys (if any) for this device.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub keys: Option<Raw<DeviceKeys>>,
236}
237
238impl DeviceListUpdateContent {
239    /// Create a new `DeviceListUpdateContent` with the given `user_id`, `device_id` and
240    /// `stream_id`.
241    pub fn new(user_id: OwnedUserId, device_id: OwnedDeviceId, stream_id: UInt) -> Self {
242        Self {
243            user_id,
244            device_id,
245            device_display_name: None,
246            stream_id,
247            prev_id: vec![],
248            deleted: None,
249            keys: None,
250        }
251    }
252}
253
254/// The description of the direct-to- device message.
255#[derive(Clone, Debug, Deserialize, Serialize)]
256#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
257pub struct DirectDeviceContent {
258    /// The user ID of the sender.
259    pub sender: OwnedUserId,
260
261    /// Event type for the message.
262    #[serde(rename = "type")]
263    pub ev_type: ToDeviceEventType,
264
265    /// Unique utf8 string ID for the message, used for idempotency.
266    pub message_id: OwnedTransactionId,
267
268    /// The contents of the messages to be sent.
269    ///
270    /// These are arranged in a map of user IDs to a map of device IDs to message bodies. The
271    /// device ID may also be *, meaning all known devices for the user.
272    pub messages: DirectDeviceMessages,
273}
274
275impl DirectDeviceContent {
276    /// Creates a new `DirectDeviceContent` with the given `sender, `ev_type` and `message_id`.
277    pub fn new(
278        sender: OwnedUserId,
279        ev_type: ToDeviceEventType,
280        message_id: OwnedTransactionId,
281    ) -> Self {
282        Self { sender, ev_type, message_id, messages: DirectDeviceMessages::new() }
283    }
284}
285
286/// Direct device message contents.
287///
288/// Represented as a map of `{ user-ids => { device-ids => message-content } }`.
289pub type DirectDeviceMessages =
290    BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>;
291
292/// The content for an `m.signing_key_update` EDU.
293#[derive(Clone, Debug, Deserialize, Serialize)]
294#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
295pub struct SigningKeyUpdateContent {
296    /// The user ID whose cross-signing keys have changed.
297    pub user_id: OwnedUserId,
298
299    /// The user's master key, if it was updated.
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub master_key: Option<Raw<CrossSigningKey>>,
302
303    /// The users's self-signing key, if it was updated.
304    #[serde(skip_serializing_if = "Option::is_none")]
305    pub self_signing_key: Option<Raw<CrossSigningKey>>,
306}
307
308impl SigningKeyUpdateContent {
309    /// Creates a new `SigningKeyUpdateContent`.
310    pub fn new(user_id: OwnedUserId) -> Self {
311        Self { user_id, master_key: None, self_signing_key: None }
312    }
313}
314
315/// An unsupported EDU type.
316#[doc(hidden)]
317#[derive(Clone, Debug, Serialize)]
318pub struct CustomEdu {
319    /// The type of EDU.
320    edu_type: String,
321
322    /// The content of the EDU.
323    content: Box<RawJsonValue>,
324}
325
326#[cfg(test)]
327mod tests {
328    use assert_matches2::assert_matches;
329    use js_int::uint;
330    use ruma_common::{room_id, user_id};
331    use ruma_events::ToDeviceEventType;
332    use serde_json::json;
333
334    use super::{DeviceListUpdateContent, Edu, ReceiptContent};
335
336    #[test]
337    fn device_list_update_edu() {
338        let json = json!({
339            "content": {
340                "deleted": false,
341                "device_display_name": "Mobile",
342                "device_id": "QBUAZIFURK",
343                "keys": {
344                    "algorithms": [
345                        "m.olm.v1.curve25519-aes-sha2",
346                        "m.megolm.v1.aes-sha2"
347                    ],
348                    "device_id": "JLAFKJWSCS",
349                    "keys": {
350                        "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
351                        "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
352                    },
353                    "signatures": {
354                        "@alice:example.com": {
355                            "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
356                        }
357                    },
358                    "user_id": "@alice:example.com"
359                },
360                "stream_id": 6,
361                "user_id": "@john:example.com"
362            },
363            "edu_type": "m.device_list_update"
364        });
365
366        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
367        assert_matches!(
368            &edu,
369            Edu::DeviceListUpdate(DeviceListUpdateContent {
370                user_id,
371                device_id,
372                device_display_name,
373                stream_id,
374                prev_id,
375                deleted,
376                keys,
377            })
378        );
379
380        assert_eq!(user_id, "@john:example.com");
381        assert_eq!(device_id, "QBUAZIFURK");
382        assert_eq!(device_display_name.as_deref(), Some("Mobile"));
383        assert_eq!(*stream_id, uint!(6));
384        assert_eq!(*prev_id, vec![]);
385        assert_eq!(*deleted, Some(false));
386        assert_matches!(keys, Some(_));
387
388        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
389    }
390
391    #[test]
392    fn minimal_device_list_update_edu() {
393        let json = json!({
394            "content": {
395                "device_id": "QBUAZIFURK",
396                "stream_id": 6,
397                "user_id": "@john:example.com"
398            },
399            "edu_type": "m.device_list_update"
400        });
401
402        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
403        assert_matches!(
404            &edu,
405            Edu::DeviceListUpdate(DeviceListUpdateContent {
406                user_id,
407                device_id,
408                device_display_name,
409                stream_id,
410                prev_id,
411                deleted,
412                keys,
413            })
414        );
415
416        assert_eq!(user_id, "@john:example.com");
417        assert_eq!(device_id, "QBUAZIFURK");
418        assert_eq!(*device_display_name, None);
419        assert_eq!(*stream_id, uint!(6));
420        assert_eq!(*prev_id, vec![]);
421        assert_eq!(*deleted, None);
422        assert_matches!(keys, None);
423
424        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
425    }
426
427    #[test]
428    fn receipt_edu() {
429        let json = json!({
430            "content": {
431                "!some_room:example.org": {
432                    "m.read": {
433                        "@john:matrix.org": {
434                            "data": {
435                                "ts": 1_533_358
436                            },
437                            "event_ids": [
438                                "$read_this_event:matrix.org"
439                            ]
440                        }
441                    }
442                }
443            },
444            "edu_type": "m.receipt"
445        });
446
447        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
448        assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
449        assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
450
451        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
452    }
453
454    #[test]
455    fn typing_edu() {
456        let json = json!({
457            "content": {
458                "room_id": "!somewhere:matrix.org",
459                "typing": true,
460                "user_id": "@john:matrix.org"
461            },
462            "edu_type": "m.typing"
463        });
464
465        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
466        assert_matches!(&edu, Edu::Typing(content));
467        assert_eq!(content.room_id, "!somewhere:matrix.org");
468        assert_eq!(content.user_id, "@john:matrix.org");
469        assert!(content.typing);
470
471        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
472    }
473
474    #[test]
475    fn direct_to_device_edu() {
476        let json = json!({
477            "content": {
478                "message_id": "hiezohf6Hoo7kaev",
479                "messages": {
480                    "@alice:example.org": {
481                        "IWHQUZUIAH": {
482                            "algorithm": "m.megolm.v1.aes-sha2",
483                            "room_id": "!Cuyf34gef24t:localhost",
484                            "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
485                            "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
486                        }
487                    }
488                },
489                "sender": "@john:example.com",
490                "type": "m.room_key_request"
491            },
492            "edu_type": "m.direct_to_device"
493        });
494
495        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
496        assert_matches!(&edu, Edu::DirectToDevice(content));
497        assert_eq!(content.sender, "@john:example.com");
498        assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
499        assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
500        assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
501
502        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
503    }
504
505    #[test]
506    fn signing_key_update_edu() {
507        let json = json!({
508            "content": {
509                "master_key": {
510                    "keys": {
511                        "ed25519:alice+base64+public+key": "alice+base64+public+key",
512                        "ed25519:base64+master+public+key": "base64+master+public+key"
513                    },
514                    "signatures": {
515                        "@alice:example.com": {
516                            "ed25519:alice+base64+master+key": "signature+of+key"
517                        }
518                    },
519                    "usage": [
520                        "master"
521                    ],
522                    "user_id": "@alice:example.com"
523                },
524                "self_signing_key": {
525                    "keys": {
526                        "ed25519:alice+base64+public+key": "alice+base64+public+key",
527                        "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
528                    },
529                    "signatures": {
530                        "@alice:example.com": {
531                            "ed25519:alice+base64+master+key": "signature+of+key",
532                            "ed25519:base64+master+public+key": "signature+of+self+signing+key"
533                        }
534                    },
535                    "usage": [
536                        "self_signing"
537                    ],
538                    "user_id": "@alice:example.com"
539                  },
540                "user_id": "@alice:example.com"
541            },
542            "edu_type": "m.signing_key_update"
543        });
544
545        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
546        assert_matches!(&edu, Edu::SigningKeyUpdate(content));
547        assert_eq!(content.user_id, "@alice:example.com");
548        assert!(content.master_key.is_some());
549        assert!(content.self_signing_key.is_some());
550
551        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
552    }
553}