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 the direct-to- device message.
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_matches2::assert_matches;
394    use js_int::uint;
395    use ruma_common::{
396        canonical_json::assert_to_canonical_json_eq, presence::PresenceState, room_id, user_id,
397    };
398    use ruma_events::ToDeviceEventType;
399    use serde_json::json;
400
401    use super::{DeviceListUpdateContent, Edu, ReceiptContent};
402
403    #[test]
404    fn device_list_update_edu() {
405        let json = json!({
406            "content": {
407                "deleted": false,
408                "device_display_name": "Mobile",
409                "device_id": "QBUAZIFURK",
410                "keys": {
411                    "algorithms": [
412                        "m.olm.v1.curve25519-aes-sha2",
413                        "m.megolm.v1.aes-sha2"
414                    ],
415                    "device_id": "JLAFKJWSCS",
416                    "keys": {
417                        "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
418                        "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
419                    },
420                    "signatures": {
421                        "@alice:example.com": {
422                            "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
423                        }
424                    },
425                    "user_id": "@alice:example.com"
426                },
427                "stream_id": 6,
428                "user_id": "@john:example.com"
429            },
430            "edu_type": "m.device_list_update"
431        });
432
433        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
434        assert_matches!(
435            &edu,
436            Edu::DeviceListUpdate(DeviceListUpdateContent {
437                user_id,
438                device_id,
439                device_display_name,
440                stream_id,
441                prev_id,
442                deleted,
443                keys,
444            })
445        );
446
447        assert_eq!(user_id, "@john:example.com");
448        assert_eq!(device_id, "QBUAZIFURK");
449        assert_eq!(device_display_name.as_deref(), Some("Mobile"));
450        assert_eq!(*stream_id, uint!(6));
451        assert_eq!(*prev_id, vec![]);
452        assert_eq!(*deleted, Some(false));
453        assert_matches!(keys, Some(_));
454
455        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
456    }
457
458    #[test]
459    fn minimal_device_list_update_edu() {
460        let json = json!({
461            "content": {
462                "device_id": "QBUAZIFURK",
463                "stream_id": 6,
464                "user_id": "@john:example.com"
465            },
466            "edu_type": "m.device_list_update"
467        });
468
469        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
470        assert_matches!(
471            &edu,
472            Edu::DeviceListUpdate(DeviceListUpdateContent {
473                user_id,
474                device_id,
475                device_display_name,
476                stream_id,
477                prev_id,
478                deleted,
479                keys,
480            })
481        );
482
483        assert_eq!(user_id, "@john:example.com");
484        assert_eq!(device_id, "QBUAZIFURK");
485        assert_eq!(*device_display_name, None);
486        assert_eq!(*stream_id, uint!(6));
487        assert_eq!(*prev_id, vec![]);
488        assert_eq!(*deleted, None);
489        assert_matches!(keys, None);
490
491        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
492    }
493
494    #[test]
495    fn receipt_edu() {
496        let json = json!({
497            "content": {
498                "!some_room:example.org": {
499                    "m.read": {
500                        "@john:matrix.org": {
501                            "data": {
502                                "ts": 1_533_358
503                            },
504                            "event_ids": [
505                                "$read_this_event:matrix.org"
506                            ]
507                        }
508                    }
509                }
510            },
511            "edu_type": "m.receipt"
512        });
513
514        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
515        assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
516        assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
517
518        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
519    }
520
521    #[test]
522    fn typing_edu() {
523        let json = json!({
524            "content": {
525                "room_id": "!somewhere:matrix.org",
526                "typing": true,
527                "user_id": "@john:matrix.org"
528            },
529            "edu_type": "m.typing"
530        });
531
532        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
533        assert_matches!(&edu, Edu::Typing(content));
534        assert_eq!(content.room_id, "!somewhere:matrix.org");
535        assert_eq!(content.user_id, "@john:matrix.org");
536        assert!(content.typing);
537
538        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
539    }
540
541    #[test]
542    fn direct_to_device_edu() {
543        let json = json!({
544            "content": {
545                "message_id": "hiezohf6Hoo7kaev",
546                "messages": {
547                    "@alice:example.org": {
548                        "IWHQUZUIAH": {
549                            "algorithm": "m.megolm.v1.aes-sha2",
550                            "room_id": "!Cuyf34gef24t:localhost",
551                            "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
552                            "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
553                        }
554                    }
555                },
556                "sender": "@john:example.com",
557                "type": "m.room_key_request"
558            },
559            "edu_type": "m.direct_to_device"
560        });
561
562        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
563        assert_matches!(&edu, Edu::DirectToDevice(content));
564        assert_eq!(content.sender, "@john:example.com");
565        assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
566        assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
567        assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
568
569        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
570    }
571
572    #[test]
573    fn signing_key_update_edu() {
574        let json = json!({
575            "content": {
576                "master_key": {
577                    "keys": {
578                        "ed25519:alice+base64+public+key": "alice+base64+public+key",
579                        "ed25519:base64+master+public+key": "base64+master+public+key"
580                    },
581                    "signatures": {
582                        "@alice:example.com": {
583                            "ed25519:alice+base64+master+key": "signature+of+key"
584                        }
585                    },
586                    "usage": [
587                        "master"
588                    ],
589                    "user_id": "@alice:example.com"
590                },
591                "self_signing_key": {
592                    "keys": {
593                        "ed25519:alice+base64+public+key": "alice+base64+public+key",
594                        "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
595                    },
596                    "signatures": {
597                        "@alice:example.com": {
598                            "ed25519:alice+base64+master+key": "signature+of+key",
599                            "ed25519:base64+master+public+key": "signature+of+self+signing+key"
600                        }
601                    },
602                    "usage": [
603                        "self_signing"
604                    ],
605                    "user_id": "@alice:example.com"
606                  },
607                "user_id": "@alice:example.com"
608            },
609            "edu_type": "m.signing_key_update"
610        });
611
612        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
613        assert_matches!(&edu, Edu::SigningKeyUpdate(content));
614        assert_eq!(content.user_id, "@alice:example.com");
615        assert!(content.master_key.is_some());
616        assert!(content.self_signing_key.is_some());
617
618        assert_eq!(serde_json::to_value(&edu).unwrap(), json);
619    }
620
621    #[test]
622    fn presence_edu() {
623        let json = json!({
624            "content": {
625                "push": [
626                    {
627                        "user_id": "@alice:example.com",
628                        "presence": "online",
629                        "currently_active": true,
630                        "last_active_ago": 1000,
631                        "status_msg": "Making cupcakes"
632                    }
633                ]
634            },
635            "edu_type": "m.presence"
636        });
637
638        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
639        assert_matches!(&edu, Edu::Presence(content));
640        assert_eq!(content.push.len(), 1);
641        let presence_update = &content.push[0];
642        assert_eq!(presence_update.user_id, "@alice:example.com");
643        assert_eq!(presence_update.presence, PresenceState::Online);
644        assert!(presence_update.currently_active);
645        assert_eq!(presence_update.last_active_ago, uint!(1000));
646        assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
647        #[cfg(feature = "unstable-msc4495")]
648        {
649            assert!(presence_update.recipients.is_empty());
650            assert!(presence_update.stream_id.is_none());
651            assert!(presence_update.prev_id.is_none());
652        }
653
654        assert_to_canonical_json_eq!(edu, json);
655    }
656
657    #[cfg(feature = "unstable-msc4495")]
658    #[test]
659    fn msc4495_presence_edu() {
660        use js_int::int;
661
662        use crate::transactions::edu::PresenceRecipientListUpdates;
663
664        let json = json!({
665            "content": {
666                "push": [
667                    {
668                        "user_id": "@alice:example.com",
669                        "presence": "online",
670                        "currently_active": true,
671                        "last_active_ago": 1000,
672                        "status_msg": "Making cupcakes",
673                        "stream_id": 321,
674                        "prev_id": 123,
675                        "recipients": {
676                            "add": ["@bob:example.com"],
677                            "delete": ["@charlie:example.com"]
678                        }
679                    }
680                ]
681            },
682            "edu_type": "m.presence"
683        });
684
685        let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
686        assert_matches!(&edu, Edu::Presence(content));
687        assert_eq!(content.push.len(), 1);
688        let presence_update = &content.push[0];
689        assert_eq!(presence_update.user_id, "@alice:example.com");
690        assert_eq!(presence_update.presence, PresenceState::Online);
691        assert!(presence_update.currently_active);
692        assert_eq!(presence_update.last_active_ago, uint!(1000));
693        assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
694        assert_eq!(presence_update.stream_id, Some(int!(321)));
695        assert_eq!(presence_update.prev_id, Some(int!(123)));
696        assert_matches!(&presence_update.recipients, PresenceRecipientListUpdates { add, delete });
697        assert_eq!(add.len(), 1);
698        assert_eq!(delete.len(), 1);
699
700        assert_to_canonical_json_eq!(edu, json);
701    }
702}