Skip to main content

ruma_client_api/sync/sync_events/
v5.rs

1//! `POST /_matrix/client/unstable/org.matrix.simplified_msc3575/sync` ([MSC4186])
2//!
3//! A simplified version of sliding sync ([MSC3575]).
4//!
5//! Get all new events in a sliding window of rooms since the last sync or a given point in time.
6//!
7//! [MSC3575]: https://github.com/matrix-org/matrix-spec-proposals/pull/3575
8//! [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
9
10use std::{collections::BTreeMap, time::Duration};
11
12use js_int::UInt;
13use js_option::JsOption;
14use ruma_common::{
15    OwnedMxcUri, OwnedRoomId, OwnedUserId,
16    api::{auth_scheme::AccessToken, request, response},
17    metadata,
18    presence::PresenceState,
19    serde::{Raw, duration::opt_ms},
20};
21use ruma_events::{AnySyncStateEvent, AnySyncTimelineEvent, StateEventType};
22use serde::{Deserialize, Serialize};
23
24use super::UnreadNotificationsCount;
25
26metadata! {
27    method: POST,
28    rate_limited: false,
29    authentication: AccessToken,
30    history: {
31        unstable("org.matrix.simplified_msc3575") => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync",
32        // 1.4 => "/_matrix/client/v5/sync",
33    }
34}
35
36/// Request type for the `/sync` endpoint.
37#[request]
38#[derive(Default)]
39pub struct Request {
40    /// A point in time to continue a sync from.
41    ///
42    /// This is an opaque value taken from the `pos` field of a previous `/sync`
43    /// response. A `None` value asks the server to start a new _session_ (mind
44    /// it can be costly)
45    #[serde(skip_serializing_if = "Option::is_none")]
46    #[ruma_api(query)]
47    pub pos: Option<String>,
48
49    /// A unique string identifier for this connection to the server.
50    ///
51    /// If this is missing, only one sliding sync connection can be made to
52    /// the server at any one time. Clients need to set this to allow more
53    /// than one connection concurrently, so the server can distinguish between
54    /// connections. This must be provided with every request, if your client
55    /// needs more than one concurrent connection.
56    ///
57    /// Limitation: it must not contain more than 16 chars, due to it being
58    /// required with every request.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub conn_id: Option<String>,
61
62    /// Allows clients to know what request params reached the server,
63    /// functionally similar to txn IDs on `/send` for events.
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub txn_id: Option<String>,
66
67    /// The maximum time to poll before responding to this request.
68    ///
69    /// `None` means no timeout, so virtually an infinite wait from the server.
70    #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")]
71    #[ruma_api(query)]
72    pub timeout: Option<Duration>,
73
74    /// Controls whether the client is automatically marked as online by polling this API.
75    ///
76    /// Defaults to `PresenceState::Online`.
77    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
78    #[ruma_api(query)]
79    pub set_presence: PresenceState,
80
81    /// Lists of rooms we are interested by, represented by ranges.
82    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
83    pub lists: BTreeMap<String, request::List>,
84
85    /// Specific rooms we are interested by.
86    ///
87    /// It is useful to receive updates from rooms that are possibly
88    /// out-of-range of all the lists (see [`Self::lists`]).
89    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
90    pub room_subscriptions: BTreeMap<OwnedRoomId, request::RoomSubscription>,
91
92    /// Extensions.
93    #[serde(default, skip_serializing_if = "request::Extensions::is_empty")]
94    pub extensions: request::Extensions,
95}
96
97impl Request {
98    /// Creates an empty `Request`.
99    pub fn new() -> Self {
100        Default::default()
101    }
102}
103
104/// HTTP types related to a [`Request`].
105pub mod request {
106    use ruma_common::{RoomId, directory::RoomTypeFilter, serde::deserialize_cow_str};
107    use serde::de::Error as _;
108
109    use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt};
110
111    /// A sliding sync list request (see [`super::Request::lists`]).
112    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
113    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
114    pub struct List {
115        /// The ranges of rooms we're interested in.
116        pub ranges: Vec<(UInt, UInt)>,
117
118        /// The details to be included per room.
119        #[serde(flatten)]
120        pub room_details: RoomDetails,
121
122        /// Filters to apply to the list before sorting.
123        #[serde(skip_serializing_if = "Option::is_none")]
124        pub filters: Option<ListFilters>,
125    }
126
127    /// A sliding sync list request filters (see [`List::filters`]).
128    ///
129    /// All fields are applied with _AND_ operators. The absence of fields
130    /// implies no filter on that criteria: it does NOT imply `false`.
131    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
132    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
133    pub struct ListFilters {
134        /// Whether to return only DM rooms (as determined by the `m.direct` account data event),
135        /// only non-DM rooms, or both.
136        #[serde(skip_serializing_if = "Option::is_none")]
137        pub is_dm: Option<bool>,
138
139        /// Whether to return only encrypted rooms (as determined by the existence of an
140        /// `m.room.encryption` state event), only unencrypted rooms, or both.
141        #[serde(skip_serializing_if = "Option::is_none")]
142        pub is_encrypted: Option<bool>,
143
144        /// Whether to return only invited rooms, only joined rooms, or both.
145        #[serde(skip_serializing_if = "Option::is_none")]
146        pub is_invite: Option<bool>,
147
148        /// Only list rooms with these create-types, or all.
149        ///
150        /// If a room type is specified in both `room_types` and `not_room_types`,
151        /// `not_room_types` wins and the corresponding rooms are not included.
152        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
153        pub room_types: Vec<RoomTypeFilter>,
154
155        /// Only list rooms that are not of these create-types, or all.
156        ///
157        /// If a room type is specified in both `room_types` and `not_room_types`,
158        /// `not_room_types` wins and the corresponding rooms are not included.
159        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
160        pub not_room_types: Vec<RoomTypeFilter>,
161    }
162
163    /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]).
164    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
165    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
166    pub struct RoomSubscription {
167        /// Required state for each returned room. An array of event type and
168        /// state key tuples.
169        #[serde(default, skip_serializing_if = "Vec::is_empty")]
170        pub required_state: Vec<(StateEventType, String)>,
171
172        /// The maximum number of timeline events to return per room.
173        pub timeline_limit: UInt,
174    }
175
176    /// Sliding sync request room details (see [`List::room_details`]).
177    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
178    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
179    pub struct RoomDetails {
180        /// Required state for each returned room. An array of event type and state key tuples.
181        #[serde(default, skip_serializing_if = "Vec::is_empty")]
182        pub required_state: Vec<(StateEventType, String)>,
183
184        /// The maximum number of timeline events to return per room.
185        pub timeline_limit: UInt,
186    }
187
188    /// Sliding sync request extensions (see [`super::Request::extensions`]).
189    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
190    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191    pub struct Extensions {
192        /// Configure the to-device extension.
193        #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
194        pub to_device: ToDevice,
195
196        /// Configure the E2EE extension.
197        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
198        pub e2ee: E2EE,
199
200        /// Configure the account data extension.
201        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
202        pub account_data: AccountData,
203
204        /// Configure the receipts extension.
205        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
206        pub receipts: Receipts,
207
208        /// Configure the typing extension.
209        #[serde(default, skip_serializing_if = "Typing::is_empty")]
210        pub typing: Typing,
211
212        /// Configure the thread subscriptions extension.
213        #[cfg(feature = "unstable-msc4308")]
214        #[serde(
215            default,
216            skip_serializing_if = "ThreadSubscriptions::is_empty",
217            rename = "io.element.msc4308.thread_subscriptions"
218        )]
219        pub thread_subscriptions: ThreadSubscriptions,
220
221        /// Configure the profiles extension.
222        #[cfg(feature = "unstable-msc4262")]
223        #[serde(
224            default,
225            skip_serializing_if = "Profiles::is_empty",
226            rename = "org.matrix.msc4262.profiles"
227        )]
228        pub profiles: Profiles,
229
230        /// Configure the sticky events extension.
231        #[cfg(feature = "unstable-msc4480")]
232        #[serde(
233            default,
234            skip_serializing_if = "StickyEvents::is_empty",
235            rename = "org.matrix.msc4354.sticky_events"
236        )]
237        pub sticky_events: StickyEvents,
238
239        /// Configure the presence extension.
240        #[cfg(feature = "unstable-msc4559")]
241        #[serde(
242            default,
243            skip_serializing_if = "Presence::is_empty",
244            rename = "org.continuwuity.presence_v2.msc4559.presence"
245        )]
246        pub presence: Presence,
247
248        /// Extensions may add further fields to the list.
249        #[serde(flatten)]
250        other: BTreeMap<String, serde_json::Value>,
251    }
252
253    impl Extensions {
254        /// Whether all fields are empty or `None`.
255        pub fn is_empty(&self) -> bool {
256            let mut empty = self.to_device.is_empty()
257                && self.e2ee.is_empty()
258                && self.account_data.is_empty()
259                && self.receipts.is_empty()
260                && self.typing.is_empty()
261                && self.other.is_empty();
262
263            #[cfg(feature = "unstable-msc4308")]
264            {
265                empty = empty && self.thread_subscriptions.is_empty();
266            }
267
268            #[cfg(feature = "unstable-msc4262")]
269            {
270                empty = empty && self.profiles.is_empty();
271            }
272
273            #[cfg(feature = "unstable-msc4480")]
274            {
275                empty = empty && self.sticky_events.is_empty();
276            }
277
278            #[cfg(feature = "unstable-msc4559")]
279            {
280                empty = empty && self.presence.is_empty();
281            }
282
283            empty
284        }
285    }
286
287    /// Single entry for a room subscription configuration in an extension request.
288    #[derive(Clone, Debug, PartialEq)]
289    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
290    pub enum ExtensionRoomConfig {
291        /// Apply extension to all global room subscriptions.
292        AllSubscribed,
293
294        /// Additionally apply extension to this specific room.
295        Room(OwnedRoomId),
296    }
297
298    impl Serialize for ExtensionRoomConfig {
299        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
300        where
301            S: serde::Serializer,
302        {
303            match self {
304                Self::AllSubscribed => serializer.serialize_str("*"),
305                Self::Room(r) => r.serialize(serializer),
306            }
307        }
308    }
309
310    impl<'de> Deserialize<'de> for ExtensionRoomConfig {
311        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
312        where
313            D: serde::de::Deserializer<'de>,
314        {
315            match deserialize_cow_str(deserializer)?.as_ref() {
316                "*" => Ok(Self::AllSubscribed),
317                other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?)),
318            }
319        }
320    }
321
322    /// To-device messages extension.
323    ///
324    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
325    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
326    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
327    pub struct ToDevice {
328        /// Activate or deactivate this extension.
329        #[serde(skip_serializing_if = "Option::is_none")]
330        pub enabled: Option<bool>,
331
332        /// Maximum number of to-device messages per response.
333        #[serde(skip_serializing_if = "Option::is_none")]
334        pub limit: Option<UInt>,
335
336        /// Give messages since this token only.
337        #[serde(skip_serializing_if = "Option::is_none")]
338        pub since: Option<String>,
339    }
340
341    impl ToDevice {
342        /// Whether all fields are empty or `None`.
343        pub fn is_empty(&self) -> bool {
344            self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
345        }
346    }
347
348    /// Sticky events extension configuration.
349    ///
350    /// According to [MSC4480](https://github.com/matrix-org/matrix-spec-proposals/pull/4480).
351    #[cfg(feature = "unstable-msc4480")]
352    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
353    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
354    pub struct StickyEvents {
355        /// Activate or deactivate this extension.
356        #[serde(skip_serializing_if = "Option::is_none")]
357        pub enabled: Option<bool>,
358
359        /// Maximum number of sticky events to return per response.
360        ///
361        /// Defaults to 100 (server-side) and the server may override it to a lower value.
362        #[serde(skip_serializing_if = "Option::is_none")]
363        pub limit: Option<UInt>,
364
365        /// Return sticky events since this token only.
366        #[serde(skip_serializing_if = "Option::is_none")]
367        pub since: Option<String>,
368    }
369
370    #[cfg(feature = "unstable-msc4480")]
371    impl StickyEvents {
372        /// Whether all fields are empty or `None`.
373        pub fn is_empty(&self) -> bool {
374            self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
375        }
376    }
377
378    /// E2EE extension configuration.
379    ///
380    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
381    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
382    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
383    pub struct E2EE {
384        /// Activate or deactivate this extension.
385        #[serde(skip_serializing_if = "Option::is_none")]
386        pub enabled: Option<bool>,
387    }
388
389    impl E2EE {
390        /// Whether all fields are empty or `None`.
391        pub fn is_empty(&self) -> bool {
392            self.enabled.is_none()
393        }
394    }
395
396    /// Account-data extension.
397    ///
398    /// Not yet part of the spec proposal. Taken from the reference implementation
399    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
400    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
401    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
402    pub struct AccountData {
403        /// Activate or deactivate this extension.
404        #[serde(skip_serializing_if = "Option::is_none")]
405        pub enabled: Option<bool>,
406
407        /// List of list names for which account data should be enabled.
408        ///
409        /// This is specific to room account data (e.g. user-defined room tags).
410        ///
411        /// If not defined, will be enabled for *all* the lists appearing in the
412        /// request. If defined and empty, will be disabled for all the lists.
413        #[serde(skip_serializing_if = "Option::is_none")]
414        pub lists: Option<Vec<String>>,
415
416        /// List of room names for which account data should be enabled.
417        ///
418        /// This is specific to room account data (e.g. user-defined room tags).
419        ///
420        /// If not defined, will be enabled for *all* the rooms appearing in the
421        /// room subscriptions. If defined and empty, will be disabled for all
422        /// the rooms.
423        #[serde(skip_serializing_if = "Option::is_none")]
424        pub rooms: Option<Vec<ExtensionRoomConfig>>,
425    }
426
427    impl AccountData {
428        /// Whether all fields are empty or `None`.
429        pub fn is_empty(&self) -> bool {
430            self.enabled.is_none()
431        }
432    }
433
434    /// Receipt extension.
435    ///
436    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
437    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
438    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
439    pub struct Receipts {
440        /// Activate or deactivate this extension.
441        #[serde(skip_serializing_if = "Option::is_none")]
442        pub enabled: Option<bool>,
443
444        /// List of list names for which receipts should be enabled.
445        ///
446        /// If not defined, will be enabled for *all* the lists appearing in the
447        /// request. If defined and empty, will be disabled for all the lists.
448        #[serde(skip_serializing_if = "Option::is_none")]
449        pub lists: Option<Vec<String>>,
450
451        /// List of room names for which receipts should be enabled.
452        ///
453        /// If not defined, will be enabled for *all* the rooms appearing in the
454        /// room subscriptions. If defined and empty, will be disabled for all
455        /// the rooms.
456        #[serde(skip_serializing_if = "Option::is_none")]
457        pub rooms: Option<Vec<ExtensionRoomConfig>>,
458    }
459
460    impl Receipts {
461        /// Whether all fields are empty or `None`.
462        pub fn is_empty(&self) -> bool {
463            self.enabled.is_none()
464        }
465    }
466
467    /// Typing extension configuration.
468    ///
469    /// Not yet part of the spec proposal. Taken from the reference implementation
470    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
471    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
472    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
473    pub struct Typing {
474        /// Activate or deactivate this extension.
475        #[serde(skip_serializing_if = "Option::is_none")]
476        pub enabled: Option<bool>,
477
478        /// List of list names for which typing notifications should be enabled.
479        ///
480        /// If not defined, will be enabled for *all* the lists appearing in the
481        /// request. If defined and empty, will be disabled for all the lists.
482        #[serde(skip_serializing_if = "Option::is_none")]
483        pub lists: Option<Vec<String>>,
484
485        /// List of room names for which typing notifications should be enabled.
486        ///
487        /// If not defined, will be enabled for *all* the rooms appearing in the
488        /// room subscriptions. If defined and empty, will be disabled for all
489        /// the rooms.
490        #[serde(skip_serializing_if = "Option::is_none")]
491        pub rooms: Option<Vec<ExtensionRoomConfig>>,
492    }
493
494    impl Typing {
495        /// Whether all fields are empty or `None`.
496        pub fn is_empty(&self) -> bool {
497            self.enabled.is_none()
498        }
499    }
500
501    /// Thread subscriptions extension.
502    ///
503    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
504    #[cfg(feature = "unstable-msc4308")]
505    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
506    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
507    pub struct ThreadSubscriptions {
508        /// Activate or deactivate this extension.
509        #[serde(skip_serializing_if = "Option::is_none")]
510        pub enabled: Option<bool>,
511
512        /// Maximum number of thread subscription changes to receive in the response.
513        ///
514        /// Defaults to 100.
515        /// Servers may impose a smaller limit than what is requested here.
516        #[serde(skip_serializing_if = "Option::is_none")]
517        pub limit: Option<UInt>,
518    }
519
520    #[cfg(feature = "unstable-msc4308")]
521    impl ThreadSubscriptions {
522        /// Whether all fields are empty or `None`.
523        pub fn is_empty(&self) -> bool {
524            self.enabled.is_none() && self.limit.is_none()
525        }
526    }
527
528    /// User profiles extension.
529    ///
530    /// Specified as part of [MSC4262](https://github.com/matrix-org/matrix-spec-proposals/pull/4262).
531    #[cfg(feature = "unstable-msc4262")]
532    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
533    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
534    pub struct Profiles {
535        /// Activate or deactivate this extension.
536        #[serde(skip_serializing_if = "Option::is_none")]
537        pub enabled: Option<bool>,
538
539        /// List of list names for which user profiles should be enabled.
540        ///
541        /// If not defined, will be enabled for *all* the lists appearing in the
542        /// request. If defined and empty, will be disabled for all the lists.
543        #[serde(skip_serializing_if = "Option::is_none")]
544        pub lists: Option<Vec<String>>,
545
546        /// List of room names for which user profiles should be enabled.
547        ///
548        /// If not defined, will be enabled for *all* the rooms appearing in the
549        /// room subscriptions. If defined and empty, will be disabled for all
550        /// the rooms.
551        #[serde(skip_serializing_if = "Option::is_none")]
552        pub rooms: Option<Vec<ExtensionRoomConfig>>,
553
554        /// Optional filter to control which profile fields to receive updates for. If omitted, all
555        /// profile field updates are included.
556        #[serde(skip_serializing_if = "Option::is_none")]
557        pub fields: Option<Vec<ruma_common::profile::ProfileFieldName>>,
558    }
559
560    #[cfg(feature = "unstable-msc4262")]
561    impl Profiles {
562        /// Whether all fields are empty or `None`.
563        pub fn is_empty(&self) -> bool {
564            self.enabled.is_none()
565        }
566    }
567
568    /// Presence extension.
569    ///
570    /// Specified as part of [MSC4559](https://github.com/matrix-org/matrix-spec-proposals/pull/4559).
571    #[cfg(feature = "unstable-msc4559")]
572    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
573    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
574    pub struct Presence {
575        /// Activate or deactivate this extension.
576        #[serde(skip_serializing_if = "Option::is_none")]
577        pub enabled: Option<bool>,
578
579        /// List of list names for which user presence should be enabled.
580        ///
581        /// If not defined, will be enabled for *all* the lists appearing in the
582        /// request. If defined and empty, will be disabled for all the lists.
583        #[serde(skip_serializing_if = "Option::is_none")]
584        pub lists: Option<Vec<String>>,
585
586        /// List of room names for which user presence should be enabled.
587        ///
588        /// If not defined, will be enabled for *all* the rooms appearing in the
589        /// room subscriptions. If defined and empty, will be disabled for all
590        /// the rooms.
591        #[serde(skip_serializing_if = "Option::is_none")]
592        pub rooms: Option<Vec<ExtensionRoomConfig>>,
593    }
594
595    #[cfg(feature = "unstable-msc4559")]
596    impl Presence {
597        /// Whether all fields are empty or `None`.
598        pub fn is_empty(&self) -> bool {
599            self.enabled.is_none()
600        }
601    }
602}
603
604/// Response type for the `/sync` endpoint.
605#[response]
606pub struct Response {
607    /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]).
608    #[serde(skip_serializing_if = "Option::is_none")]
609    pub txn_id: Option<String>,
610
611    /// The token to supply in the `pos` parameter of the next `/sync` request
612    /// (see [`Request::pos`]).
613    pub pos: String,
614
615    /// Resulting details of the lists.
616    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
617    pub lists: BTreeMap<String, response::List>,
618
619    /// The updated rooms.
620    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
621    pub rooms: BTreeMap<OwnedRoomId, response::Room>,
622
623    /// Extensions.
624    #[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
625    pub extensions: response::Extensions,
626}
627
628impl Response {
629    /// Creates a new `Response` with the given `pos`.
630    pub fn new(pos: String) -> Self {
631        Self {
632            txn_id: None,
633            pos,
634            lists: Default::default(),
635            rooms: Default::default(),
636            extensions: Default::default(),
637        }
638    }
639}
640
641/// HTTP types related to a [`Response`].
642pub mod response {
643    use ruma_common::OneTimeKeyAlgorithm;
644    #[cfg(feature = "unstable-msc4308")]
645    use ruma_common::OwnedEventId;
646    #[cfg(feature = "unstable-msc4559")]
647    use ruma_common::presence::{PresenceState, PresenceStatus};
648    #[cfg(feature = "unstable-msc4262")]
649    use ruma_common::profile::UserProfileUpdate;
650    use ruma_events::{
651        AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
652        AnyToDeviceEvent, receipt::SyncReceiptEvent, typing::SyncTypingEvent,
653    };
654
655    use super::{
656        super::DeviceLists, AnySyncStateEvent, AnySyncTimelineEvent, BTreeMap, Deserialize,
657        JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, UInt,
658        UnreadNotificationsCount,
659    };
660    #[cfg(feature = "unstable-msc4308")]
661    use crate::threads::get_thread_subscriptions_changes::unstable::{
662        ThreadSubscription, ThreadUnsubscription,
663    };
664
665    /// A sliding sync response updates to joiend rooms (see
666    /// [`super::Response::lists`]).
667    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
668    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
669    pub struct List {
670        /// The total number of rooms found for this list.
671        pub count: UInt,
672    }
673
674    /// A sliding sync response updated room (see [`super::Response::rooms`]).
675    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
676    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
677    pub struct Room {
678        /// The name as calculated by the server.
679        ///
680        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
681        /// this field is ignored if its deserialization fails.
682        #[serde(skip_serializing_if = "Option::is_none")]
683        #[cfg_attr(
684            feature = "unstable-compat-lax-syncv5-deser",
685            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
686        )]
687        pub name: Option<String>,
688
689        /// The avatar.
690        ///
691        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
692        /// this field is ignored if its deserialization fails.
693        #[serde(default, skip_serializing_if = "JsOption::is_undefined")]
694        #[cfg_attr(
695            feature = "unstable-compat-lax-syncv5-deser",
696            serde(deserialize_with = "ruma_common::serde::default_on_error")
697        )]
698        pub avatar: JsOption<OwnedMxcUri>,
699
700        /// Whether it is an initial response.
701        #[serde(skip_serializing_if = "Option::is_none")]
702        pub initial: Option<bool>,
703
704        /// Whether it is a direct room.
705        #[serde(skip_serializing_if = "Option::is_none")]
706        pub is_dm: Option<bool>,
707
708        /// If this is `Some(_)`, this is a not-yet-accepted invite containing
709        /// the given stripped state events.
710        #[serde(skip_serializing_if = "Option::is_none")]
711        pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
712
713        /// Number of unread notifications.
714        #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
715        pub unread_notifications: UnreadNotificationsCount,
716
717        /// Message-like events and live state events.
718        #[serde(default, skip_serializing_if = "Vec::is_empty")]
719        pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
720
721        /// State events as configured by the request.
722        #[serde(default, skip_serializing_if = "Vec::is_empty")]
723        pub required_state: Vec<Raw<AnySyncStateEvent>>,
724
725        /// The `prev_batch` allowing you to paginate through the messages
726        /// before the given ones.
727        #[serde(skip_serializing_if = "Option::is_none")]
728        pub prev_batch: Option<String>,
729
730        /// True if the number of events returned was limited by the limit on
731        /// the filter.
732        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
733        pub limited: bool,
734
735        /// The number of users with membership of `join`, including the
736        /// client’s own user ID.
737        #[serde(skip_serializing_if = "Option::is_none")]
738        pub joined_count: Option<UInt>,
739
740        /// The number of users with membership of `invite`.
741        #[serde(skip_serializing_if = "Option::is_none")]
742        pub invited_count: Option<UInt>,
743
744        /// The number of timeline events which have just occurred and are not
745        /// historical.
746        #[serde(skip_serializing_if = "Option::is_none")]
747        pub num_live: Option<UInt>,
748
749        /// The bump stamp of the room.
750        ///
751        /// It can be interpreted as a “recency stamp” or “streaming order
752        /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB`
753        /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC`
754        /// receives an update, its `bump_stamp` will be 3.
755        #[serde(skip_serializing_if = "Option::is_none")]
756        pub bump_stamp: Option<UInt>,
757
758        /// Heroes of the room.
759        #[serde(skip_serializing_if = "Option::is_none")]
760        pub heroes: Option<Vec<Hero>>,
761    }
762
763    impl Room {
764        /// Creates an empty `Room`.
765        pub fn new() -> Self {
766            Default::default()
767        }
768    }
769
770    /// A sliding sync response room hero (see [`Room::heroes`]).
771    #[derive(Clone, Debug, Deserialize, Serialize)]
772    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
773    pub struct Hero {
774        /// The user ID.
775        pub user_id: OwnedUserId,
776
777        /// The name.
778        ///
779        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
780        /// this field is ignored if its deserialization fails.
781        #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
782        #[cfg_attr(
783            feature = "unstable-compat-lax-syncv5-deser",
784            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
785        )]
786        pub name: Option<String>,
787
788        /// The avatar.
789        ///
790        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
791        /// this field is ignored if its deserialization fails.
792        #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
793        #[cfg_attr(
794            feature = "unstable-compat-lax-syncv5-deser",
795            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
796        )]
797        pub avatar: Option<OwnedMxcUri>,
798    }
799
800    impl Hero {
801        /// Creates a new `Hero` with the given user ID.
802        pub fn new(user_id: OwnedUserId) -> Self {
803            Self { user_id, name: None, avatar: None }
804        }
805    }
806
807    /// Extensions responses.
808    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
809    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
810    pub struct Extensions {
811        /// To-device extension response.
812        #[serde(skip_serializing_if = "Option::is_none")]
813        pub to_device: Option<ToDevice>,
814
815        /// E2EE extension response.
816        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
817        pub e2ee: E2EE,
818
819        /// Account data extension response.
820        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
821        pub account_data: AccountData,
822
823        /// Receipts extension response.
824        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
825        pub receipts: Receipts,
826
827        /// Typing extension response.
828        #[serde(default, skip_serializing_if = "Typing::is_empty")]
829        pub typing: Typing,
830
831        /// Thread subscriptions extension response.
832        #[cfg(feature = "unstable-msc4308")]
833        #[serde(
834            default,
835            skip_serializing_if = "ThreadSubscriptions::is_empty",
836            rename = "io.element.msc4308.thread_subscriptions"
837        )]
838        pub thread_subscriptions: ThreadSubscriptions,
839
840        /// Profiles extension response.
841        #[cfg(feature = "unstable-msc4262")]
842        #[serde(
843            default,
844            skip_serializing_if = "Profiles::is_empty",
845            rename = "org.matrix.msc4262.profiles"
846        )]
847        pub profiles: Profiles,
848
849        /// Sticky events extension response.
850        #[cfg(feature = "unstable-msc4480")]
851        #[serde(
852            default,
853            skip_serializing_if = "StickyEvents::is_empty",
854            rename = "org.matrix.msc4354.sticky_events"
855        )]
856        pub sticky_events: StickyEvents,
857
858        /// Presence extension response.
859        #[cfg(feature = "unstable-msc4559")]
860        #[serde(
861            default,
862            skip_serializing_if = "Presence::is_empty",
863            rename = "org.continuwuity.presence_v2.msc4559.presence"
864        )]
865        pub presence: Presence,
866    }
867
868    impl Extensions {
869        /// Whether the extension data is empty.
870        ///
871        /// True if neither to-device, e2ee nor account data are to be found.
872        pub fn is_empty(&self) -> bool {
873            let mut empty = self.to_device.is_none()
874                && self.e2ee.is_empty()
875                && self.account_data.is_empty()
876                && self.receipts.is_empty()
877                && self.typing.is_empty();
878
879            #[cfg(feature = "unstable-msc4308")]
880            {
881                empty = empty && self.thread_subscriptions.is_empty();
882            }
883
884            #[cfg(feature = "unstable-msc4262")]
885            {
886                empty = empty && self.profiles.is_empty();
887            }
888
889            #[cfg(feature = "unstable-msc4480")]
890            {
891                empty = empty && self.sticky_events.is_empty();
892            }
893
894            #[cfg(feature = "unstable-msc4559")]
895            {
896                empty = empty && self.presence.is_empty();
897            }
898
899            empty
900        }
901    }
902
903    /// To-device extension response.
904    ///
905    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
906    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
907    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
908    pub struct ToDevice {
909        /// Fetch the next batch from this entry.
910        pub next_batch: String,
911
912        /// The to-device events.
913        #[serde(default, skip_serializing_if = "Vec::is_empty")]
914        pub events: Vec<Raw<AnyToDeviceEvent>>,
915    }
916
917    /// Sticky events extension response.
918    ///
919    /// According to [MSC4480](https://github.com/matrix-org/matrix-spec-proposals/pull/4480).
920    #[cfg(feature = "unstable-msc4480")]
921    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
922    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
923    pub struct StickyEvents {
924        /// The token to supply in the `since` param of the next request.
925        ///
926        /// Set when there are changes.
927        #[serde(skip_serializing_if = "Option::is_none")]
928        pub next_batch: Option<String>,
929
930        /// The sticky events, grouped by room.
931        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
932        pub rooms: BTreeMap<OwnedRoomId, StickyEventsRoom>,
933    }
934
935    #[cfg(feature = "unstable-msc4480")]
936    impl StickyEvents {
937        /// Whether the extension data is empty.
938        pub fn is_empty(&self) -> bool {
939            self.next_batch.is_none() && self.rooms.is_empty()
940        }
941    }
942
943    /// Sticky events for a single room in the sticky events extension response.
944    #[cfg(feature = "unstable-msc4480")]
945    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
946    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
947    pub struct StickyEventsRoom {
948        /// The sticky events for this room.
949        #[serde(default, skip_serializing_if = "Vec::is_empty")]
950        pub events: Vec<Raw<AnySyncTimelineEvent>>,
951    }
952
953    /// E2EE extension response.
954    ///
955    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
956    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
957    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
958    pub struct E2EE {
959        /// Information on E2EE device updates.
960        #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
961        pub device_lists: DeviceLists,
962
963        /// For each key algorithm, the number of unclaimed one-time keys
964        /// currently held on the server for a device.
965        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
966        pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
967
968        /// The unused fallback key algorithms.
969        ///
970        /// The presence of this field indicates that the server supports
971        /// fallback keys.
972        #[serde(skip_serializing_if = "Option::is_none")]
973        pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
974    }
975
976    impl E2EE {
977        /// Whether all fields are empty or `None`.
978        pub fn is_empty(&self) -> bool {
979            self.device_lists.is_empty()
980                && self.device_one_time_keys_count.is_empty()
981                && self.device_unused_fallback_key_types.is_none()
982        }
983    }
984
985    /// Account-data extension response .
986    ///
987    /// Not yet part of the spec proposal. Taken from the reference implementation
988    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
989    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
990    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
991    pub struct AccountData {
992        /// The global private data created by this user.
993        #[serde(default, skip_serializing_if = "Vec::is_empty")]
994        pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
995
996        /// The private data that this user has attached to each room.
997        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
998        pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
999    }
1000
1001    impl AccountData {
1002        /// Whether all fields are empty or `None`.
1003        pub fn is_empty(&self) -> bool {
1004            self.global.is_empty() && self.rooms.is_empty()
1005        }
1006    }
1007
1008    /// Receipt extension response.
1009    ///
1010    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
1011    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1012    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1013    pub struct Receipts {
1014        /// The ephemeral receipt room event for each room.
1015        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1016        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
1017    }
1018
1019    impl Receipts {
1020        /// Whether all fields are empty or `None`.
1021        pub fn is_empty(&self) -> bool {
1022            self.rooms.is_empty()
1023        }
1024    }
1025
1026    /// Typing extension response.
1027    ///
1028    /// Not yet part of the spec proposal. Taken from the reference implementation
1029    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
1030    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1031    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1032    pub struct Typing {
1033        /// The ephemeral typing event for each room.
1034        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1035        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
1036    }
1037
1038    impl Typing {
1039        /// Whether all fields are empty or `None`.
1040        pub fn is_empty(&self) -> bool {
1041            self.rooms.is_empty()
1042        }
1043    }
1044
1045    /// Thread subscriptions extension response.
1046    ///
1047    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
1048    #[cfg(feature = "unstable-msc4308")]
1049    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1050    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1051    pub struct ThreadSubscriptions {
1052        /// New thread subscriptions.
1053        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1054        pub subscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadSubscription>>,
1055
1056        /// New thread unsubscriptions.
1057        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1058        pub unsubscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadUnsubscription>>,
1059
1060        /// A token that can be used to backpaginate (via the companion endpoint) other thread
1061        /// subscription changes that occurred since the last sync, but that were not included in
1062        /// this response.
1063        ///
1064        /// Only set when there are more changes to fetch.
1065        #[serde(skip_serializing_if = "Option::is_none")]
1066        pub prev_batch: Option<String>,
1067    }
1068
1069    #[cfg(feature = "unstable-msc4308")]
1070    impl ThreadSubscriptions {
1071        /// Whether all fields are empty or `None`.
1072        pub fn is_empty(&self) -> bool {
1073            self.subscribed.is_empty() && self.unsubscribed.is_empty() && self.prev_batch.is_none()
1074        }
1075    }
1076
1077    /// Profiles extension response.
1078    ///
1079    /// Specified as part of [MSC4262](https://github.com/matrix-org/matrix-spec-proposals/pull/4262).
1080    #[cfg(feature = "unstable-msc4262")]
1081    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1082    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1083    pub struct Profiles {
1084        /// Profile updates keyed by user ID.
1085        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1086        pub users: BTreeMap<OwnedUserId, UserProfileUpdate>,
1087    }
1088
1089    #[cfg(feature = "unstable-msc4262")]
1090    impl Profiles {
1091        /// Whether all fields are empty or `None`.
1092        pub fn is_empty(&self) -> bool {
1093            self.users.is_empty()
1094        }
1095    }
1096
1097    /// An update to a user's presence sent down sync.
1098    ///
1099    /// As defined in [MSC4559](https://github.com/matrix-org/matrix-spec-proposals/pull/4559).
1100    #[cfg(feature = "unstable-msc4559")]
1101    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1102    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1103    pub struct PresenceUpdate {
1104        /// The user's presence state.
1105        pub presence: PresenceState,
1106
1107        /// The status information for this user's presence, using the format from [MSC4532].
1108        ///
1109        /// [MSC4532]: https://github.com/matrix-org/matrix-spec-proposals/pull/4532
1110        pub status: PresenceStatus,
1111    }
1112
1113    /// Presence extension response.
1114    ///
1115    /// Specified as part of [MSC4559](https://github.com/matrix-org/matrix-spec-proposals/pull/4559).
1116    #[cfg(feature = "unstable-msc4559")]
1117    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
1118    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
1119    pub struct Presence {
1120        /// Presence updates keyed by user ID.
1121        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1122        pub user_ids: BTreeMap<OwnedUserId, PresenceUpdate>,
1123    }
1124
1125    #[cfg(feature = "unstable-msc4559")]
1126    impl Presence {
1127        /// Whether all fields are empty or `None`.
1128        pub fn is_empty(&self) -> bool {
1129            self.user_ids.is_empty()
1130        }
1131    }
1132}
1133
1134#[cfg(test)]
1135mod tests {
1136    use ruma_common::owned_room_id;
1137
1138    use super::request::ExtensionRoomConfig;
1139
1140    #[test]
1141    fn serialize_request_extension_room_config() {
1142        let entry = ExtensionRoomConfig::AllSubscribed;
1143        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
1144
1145        let entry = ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"));
1146        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
1147    }
1148
1149    #[test]
1150    fn deserialize_request_extension_room_config() {
1151        assert_eq!(
1152            serde_json::from_str::<ExtensionRoomConfig>(r#""*""#).unwrap(),
1153            ExtensionRoomConfig::AllSubscribed
1154        );
1155
1156        assert_eq!(
1157            serde_json::from_str::<ExtensionRoomConfig>(r#""!foo:bar.baz""#).unwrap(),
1158            ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"))
1159        );
1160    }
1161
1162    #[cfg(feature = "unstable-msc4480")]
1163    #[test]
1164    fn sticky_events_extension_serde() {
1165        use ruma_common::assert_to_canonical_json_eq;
1166
1167        use super::{request, response};
1168
1169        // The request extension serializes under the unstable extension key.
1170        let mut extensions = request::Extensions::default();
1171        extensions.sticky_events =
1172            request::StickyEvents { enabled: Some(true), ..Default::default() };
1173        assert_to_canonical_json_eq!(
1174            extensions,
1175            serde_json::json!({ "org.matrix.msc4354.sticky_events": { "enabled": true } })
1176        );
1177
1178        // The response extension and its per-room sticky events deserialize.
1179        let response: response::Extensions = serde_json::from_value(serde_json::json!({
1180            "org.matrix.msc4354.sticky_events": {
1181                "next_batch": "s123",
1182                "rooms": {
1183                    "!room:example.com": {
1184                        "events": [{
1185                            "content": { "body": "sticky", "msgtype": "m.text" },
1186                            "event_id": "$1:example.com",
1187                            "origin_server_ts": 1,
1188                            "sender": "@alice:example.com",
1189                            "type": "m.room.message",
1190                            "msc4354_sticky": {
1191                                "duration_ms": 300_000
1192                            },
1193                        }]
1194                    }
1195                }
1196            }
1197        }))
1198        .unwrap();
1199
1200        assert_eq!(response.sticky_events.next_batch.as_deref(), Some("s123"));
1201        assert_eq!(response.sticky_events.rooms.len(), 1);
1202        let room = response.sticky_events.rooms.values().next().unwrap();
1203        assert_eq!(room.events.len(), 1);
1204    }
1205
1206    #[test]
1207    #[cfg(feature = "unstable-compat-lax-syncv5-deser")]
1208    fn deserialize_room_ignores_invalid_string_fields() {
1209        use super::response::Room;
1210
1211        let room: Room = serde_json::from_str(
1212            r#"{
1213                "name": {},
1214                "avatar": {},
1215                "heroes": [{ "user_id": "@alice:localhost", "displayname": {}, "avatar_url": {} }]
1216            }"#,
1217        )
1218        .unwrap();
1219
1220        assert_eq!(room.name, None);
1221        assert!(room.avatar.is_undefined());
1222        let hero = &room.heroes.unwrap()[0];
1223        assert_eq!(hero.name, None);
1224        assert_eq!(hero.avatar, None);
1225
1226        // Valid values are still kept.
1227        let room: Room = serde_json::from_str(
1228            r#"{ "name": "Room", "avatar": "mxc://localhost/a", "heroes": [{ "user_id": "@alice:localhost", "displayname": "Alice", "avatar_url": "mxc://localhost/b" }] }"#,
1229        )
1230        .unwrap();
1231
1232        assert_eq!(room.name.as_deref(), Some("Room"));
1233        assert_eq!(room.avatar.into_option().unwrap().as_str(), "mxc://localhost/a");
1234        let hero = &room.heroes.unwrap()[0];
1235        assert_eq!(hero.name.as_deref(), Some("Alice"));
1236        assert_eq!(hero.avatar.as_ref().unwrap().as_str(), "mxc://localhost/b");
1237    }
1238}