Skip to main content

ruma_federation_api/transactions/
edu.rs

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