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