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    api::{request, response, Metadata},
16    metadata,
17    serde::{duration::opt_ms, Raw},
18    OwnedMxcUri, OwnedRoomId, OwnedUserId,
19};
20use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType};
21use serde::{Deserialize, Serialize};
22
23use super::UnreadNotificationsCount;
24
25const METADATA: Metadata = metadata! {
26    method: POST,
27    rate_limited: false,
28    authentication: AccessToken,
29    history: {
30        unstable => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync",
31        // 1.4 => "/_matrix/client/v5/sync",
32    }
33};
34
35/// Request type for the `/sync` endpoint.
36#[request(error = crate::Error)]
37#[derive(Default)]
38pub struct Request {
39    /// A point in time to continue a sync from.
40    ///
41    /// This is an opaque value taken from the `pos` field of a previous `/sync`
42    /// response. A `None` value asks the server to start a new _session_ (mind
43    /// it can be costly)
44    #[serde(skip_serializing_if = "Option::is_none")]
45    #[ruma_api(query)]
46    pub pos: Option<String>,
47
48    /// A unique string identifier for this connection to the server.
49    ///
50    /// If this is missing, only one sliding sync connection can be made to
51    /// the server at any one time. Clients need to set this to allow more
52    /// than one connection concurrently, so the server can distinguish between
53    /// connections. This must be provided with every request, if your client
54    /// needs more than one concurrent connection.
55    ///
56    /// Limitation: it must not contain more than 16 chars, due to it being
57    /// required with every request.
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub conn_id: Option<String>,
60
61    /// Allows clients to know what request params reached the server,
62    /// functionally similar to txn IDs on `/send` for events.
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub txn_id: Option<String>,
65
66    /// The maximum time to poll before responding to this request.
67    ///
68    /// `None` means no timeout, so virtually an infinite wait from the server.
69    #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")]
70    #[ruma_api(query)]
71    pub timeout: Option<Duration>,
72
73    /// Lists of rooms we are interested by, represented by ranges.
74    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
75    pub lists: BTreeMap<String, request::List>,
76
77    /// Specific rooms we are interested by.
78    ///
79    /// It is useful to receive updates from rooms that are possibly
80    /// out-of-range of all the lists (see [`Self::lists`]).
81    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
82    pub room_subscriptions: BTreeMap<OwnedRoomId, request::RoomSubscription>,
83
84    /// Extensions.
85    #[serde(default, skip_serializing_if = "request::Extensions::is_empty")]
86    pub extensions: request::Extensions,
87}
88
89impl Request {
90    /// Creates an empty `Request`.
91    pub fn new() -> Self {
92        Default::default()
93    }
94}
95
96/// HTTP types related to a [`Request`].
97pub mod request {
98    use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId};
99    use serde::de::Error as _;
100
101    use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt};
102
103    /// A sliding sync list request (see [`super::Request::lists`]).
104    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
105    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
106    pub struct List {
107        /// The ranges of rooms we're interested in.
108        pub ranges: Vec<(UInt, UInt)>,
109
110        /// The details to be included per room.
111        #[serde(flatten)]
112        pub room_details: RoomDetails,
113
114        /// Request a stripped variant of membership events for the users used
115        /// to calculate the room name.
116        #[serde(skip_serializing_if = "Option::is_none")]
117        pub include_heroes: Option<bool>,
118
119        /// Filters to apply to the list before sorting.
120        #[serde(skip_serializing_if = "Option::is_none")]
121        pub filters: Option<ListFilters>,
122    }
123
124    /// A sliding sync list request filters (see [`List::filters`]).
125    ///
126    /// All fields are applied with _AND_ operators. The absence of fields
127    /// implies no filter on that criteria: it does NOT imply `false`.
128    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
129    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
130    pub struct ListFilters {
131        /// Whether to return invited rooms, only joined rooms or both.
132        ///
133        /// Flag which only returns rooms the user is currently invited to.
134        /// If unset, both invited and joined rooms are returned. If false,
135        /// no invited rooms are returned. If true, only invited rooms are
136        /// returned.
137        #[serde(skip_serializing_if = "Option::is_none")]
138        pub is_invite: Option<bool>,
139
140        /// Only list rooms that are not of these create-types, or all.
141        ///
142        /// This can be used to filter out spaces from the room list.
143        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
144        pub not_room_types: Vec<RoomTypeFilter>,
145    }
146
147    /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]).
148    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
149    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
150    pub struct RoomSubscription {
151        /// Required state for each returned room. An array of event type and
152        /// state key tuples.
153        #[serde(default, skip_serializing_if = "Vec::is_empty")]
154        pub required_state: Vec<(StateEventType, String)>,
155
156        /// The maximum number of timeline events to return per room.
157        pub timeline_limit: UInt,
158
159        /// Include the room heroes.
160        #[serde(skip_serializing_if = "Option::is_none")]
161        pub include_heroes: Option<bool>,
162    }
163
164    /// Sliding sync request room details (see [`List::room_details`]).
165    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
166    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
167    pub struct RoomDetails {
168        /// Required state for each returned room. An array of event type and 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 extensions (see [`super::Request::extensions`]).
177    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
178    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
179    pub struct Extensions {
180        /// Configure the to-device extension.
181        #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
182        pub to_device: ToDevice,
183
184        /// Configure the E2EE extension.
185        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
186        pub e2ee: E2EE,
187
188        /// Configure the account data extension.
189        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
190        pub account_data: AccountData,
191
192        /// Configure the receipts extension.
193        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
194        pub receipts: Receipts,
195
196        /// Configure the typing extension.
197        #[serde(default, skip_serializing_if = "Typing::is_empty")]
198        pub typing: Typing,
199
200        /// Extensions may add further fields to the list.
201        #[serde(flatten)]
202        other: BTreeMap<String, serde_json::Value>,
203    }
204
205    impl Extensions {
206        /// Whether all fields are empty or `None`.
207        pub fn is_empty(&self) -> bool {
208            self.to_device.is_empty()
209                && self.e2ee.is_empty()
210                && self.account_data.is_empty()
211                && self.receipts.is_empty()
212                && self.typing.is_empty()
213                && self.other.is_empty()
214        }
215    }
216
217    /// Single entry for a room subscription configuration in an extension request.
218    #[derive(Clone, Debug, PartialEq)]
219    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
220    pub enum ExtensionRoomConfig {
221        /// Apply extension to all global room subscriptions.
222        AllSubscribed,
223
224        /// Additionally apply extension to this specific room.
225        Room(OwnedRoomId),
226    }
227
228    impl Serialize for ExtensionRoomConfig {
229        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
230        where
231            S: serde::Serializer,
232        {
233            match self {
234                Self::AllSubscribed => serializer.serialize_str("*"),
235                Self::Room(r) => r.serialize(serializer),
236            }
237        }
238    }
239
240    impl<'de> Deserialize<'de> for ExtensionRoomConfig {
241        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
242        where
243            D: serde::de::Deserializer<'de>,
244        {
245            match deserialize_cow_str(deserializer)?.as_ref() {
246                "*" => Ok(Self::AllSubscribed),
247                other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())),
248            }
249        }
250    }
251
252    /// To-device messages extension.
253    ///
254    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
255    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
256    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
257    pub struct ToDevice {
258        /// Activate or deactivate this extension.
259        #[serde(skip_serializing_if = "Option::is_none")]
260        pub enabled: Option<bool>,
261
262        /// Maximum number of to-device messages per response.
263        #[serde(skip_serializing_if = "Option::is_none")]
264        pub limit: Option<UInt>,
265
266        /// Give messages since this token only.
267        #[serde(skip_serializing_if = "Option::is_none")]
268        pub since: Option<String>,
269    }
270
271    impl ToDevice {
272        /// Whether all fields are empty or `None`.
273        pub fn is_empty(&self) -> bool {
274            self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
275        }
276    }
277
278    /// E2EE extension configuration.
279    ///
280    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
281    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
282    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
283    pub struct E2EE {
284        /// Activate or deactivate this extension.
285        #[serde(skip_serializing_if = "Option::is_none")]
286        pub enabled: Option<bool>,
287    }
288
289    impl E2EE {
290        /// Whether all fields are empty or `None`.
291        pub fn is_empty(&self) -> bool {
292            self.enabled.is_none()
293        }
294    }
295
296    /// Account-data extension .
297    ///
298    /// Not yet part of the spec proposal. Taken from the reference implementation
299    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
300    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
301    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
302    pub struct AccountData {
303        /// Activate or deactivate this extension.
304        #[serde(skip_serializing_if = "Option::is_none")]
305        pub enabled: Option<bool>,
306
307        /// List of list names for which account data should be enabled.
308        ///
309        /// This is specific to room account data (e.g. user-defined room tags).
310        ///
311        /// If not defined, will be enabled for *all* the lists appearing in the
312        /// request. If defined and empty, will be disabled for all the lists.
313        #[serde(skip_serializing_if = "Option::is_none")]
314        pub lists: Option<Vec<String>>,
315
316        /// List of room names for which account data should be enabled.
317        ///
318        /// This is specific to room account data (e.g. user-defined room tags).
319        ///
320        /// If not defined, will be enabled for *all* the rooms appearing in the
321        /// room subscriptions. If defined and empty, will be disabled for all
322        /// the rooms.
323        #[serde(skip_serializing_if = "Option::is_none")]
324        pub rooms: Option<Vec<ExtensionRoomConfig>>,
325    }
326
327    impl AccountData {
328        /// Whether all fields are empty or `None`.
329        pub fn is_empty(&self) -> bool {
330            self.enabled.is_none()
331        }
332    }
333
334    /// Receipt extension.
335    ///
336    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
337    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
338    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
339    pub struct Receipts {
340        /// Activate or deactivate this extension.
341        #[serde(skip_serializing_if = "Option::is_none")]
342        pub enabled: Option<bool>,
343
344        /// List of list names for which receipts should be enabled.
345        ///
346        /// If not defined, will be enabled for *all* the lists appearing in the
347        /// request. If defined and empty, will be disabled for all the lists.
348        #[serde(skip_serializing_if = "Option::is_none")]
349        pub lists: Option<Vec<String>>,
350
351        /// List of room names for which receipts should be enabled.
352        ///
353        /// If not defined, will be enabled for *all* the rooms appearing in the
354        /// room subscriptions. If defined and empty, will be disabled for all
355        /// the rooms.
356        #[serde(skip_serializing_if = "Option::is_none")]
357        pub rooms: Option<Vec<ExtensionRoomConfig>>,
358    }
359
360    impl Receipts {
361        /// Whether all fields are empty or `None`.
362        pub fn is_empty(&self) -> bool {
363            self.enabled.is_none()
364        }
365    }
366
367    /// Typing extension configuration.
368    ///
369    /// Not yet part of the spec proposal. Taken from the reference implementation
370    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
371    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
372    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
373    pub struct Typing {
374        /// Activate or deactivate this extension.
375        #[serde(skip_serializing_if = "Option::is_none")]
376        pub enabled: Option<bool>,
377
378        /// List of list names for which typing notifications should be enabled.
379        ///
380        /// If not defined, will be enabled for *all* the lists appearing in the
381        /// request. If defined and empty, will be disabled for all the lists.
382        #[serde(skip_serializing_if = "Option::is_none")]
383        pub lists: Option<Vec<String>>,
384
385        /// List of room names for which typing notifications should be enabled.
386        ///
387        /// If not defined, will be enabled for *all* the rooms appearing in the
388        /// room subscriptions. If defined and empty, will be disabled for all
389        /// the rooms.
390        #[serde(skip_serializing_if = "Option::is_none")]
391        pub rooms: Option<Vec<ExtensionRoomConfig>>,
392    }
393
394    impl Typing {
395        /// Whether all fields are empty or `None`.
396        pub fn is_empty(&self) -> bool {
397            self.enabled.is_none()
398        }
399    }
400}
401
402/// Response type for the `/sync` endpoint.
403#[response(error = crate::Error)]
404pub struct Response {
405    /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]).
406    #[serde(skip_serializing_if = "Option::is_none")]
407    pub txn_id: Option<String>,
408
409    /// The token to supply in the `pos` parameter of the next `/sync` request
410    /// (see [`Request::pos`]).
411    pub pos: String,
412
413    /// Resulting details of the lists.
414    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
415    pub lists: BTreeMap<String, response::List>,
416
417    /// The updated rooms.
418    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
419    pub rooms: BTreeMap<OwnedRoomId, response::Room>,
420
421    /// Extensions.
422    #[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
423    pub extensions: response::Extensions,
424}
425
426impl Response {
427    /// Creates a new `Response` with the given `pos`.
428    pub fn new(pos: String) -> Self {
429        Self {
430            txn_id: None,
431            pos,
432            lists: Default::default(),
433            rooms: Default::default(),
434            extensions: Default::default(),
435        }
436    }
437}
438
439/// HTTP types related to a [`Response`].
440pub mod response {
441    use ruma_common::OneTimeKeyAlgorithm;
442    use ruma_events::{
443        receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent,
444        AnyRoomAccountDataEvent, AnyToDeviceEvent,
445    };
446
447    use super::{
448        super::DeviceLists, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
449        BTreeMap, Deserialize, JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize,
450        UInt, UnreadNotificationsCount,
451    };
452
453    /// A sliding sync response updates to joiend rooms (see
454    /// [`super::Response::lists`]).
455    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
456    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
457    pub struct List {
458        /// The total number of rooms found for this list.
459        pub count: UInt,
460    }
461
462    /// A slising sync response updated room (see [`super::Response::rooms`]).
463    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
464    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
465    pub struct Room {
466        /// The name as calculated by the server.
467        #[serde(skip_serializing_if = "Option::is_none")]
468        pub name: Option<String>,
469
470        /// The avatar.
471        #[serde(default, skip_serializing_if = "JsOption::is_undefined")]
472        pub avatar: JsOption<OwnedMxcUri>,
473
474        /// Whether it is an initial response.
475        #[serde(skip_serializing_if = "Option::is_none")]
476        pub initial: Option<bool>,
477
478        /// Whether it is a direct room.
479        #[serde(skip_serializing_if = "Option::is_none")]
480        pub is_dm: Option<bool>,
481
482        /// If this is `Some(_)`, this is a not-yet-accepted invite containing
483        /// the given stripped state events.
484        #[serde(skip_serializing_if = "Option::is_none")]
485        pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
486
487        /// Number of unread notifications.
488        #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
489        pub unread_notifications: UnreadNotificationsCount,
490
491        /// Message-like events and live state events.
492        #[serde(default, skip_serializing_if = "Vec::is_empty")]
493        pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
494
495        /// State events as configured by the request.
496        #[serde(default, skip_serializing_if = "Vec::is_empty")]
497        pub required_state: Vec<Raw<AnySyncStateEvent>>,
498
499        /// The `prev_batch` allowing you to paginate through the messages
500        /// before the given ones.
501        #[serde(skip_serializing_if = "Option::is_none")]
502        pub prev_batch: Option<String>,
503
504        /// True if the number of events returned was limited by the limit on
505        /// the filter.
506        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
507        pub limited: bool,
508
509        /// The number of users with membership of `join`, including the
510        /// client’s own user ID.
511        #[serde(skip_serializing_if = "Option::is_none")]
512        pub joined_count: Option<UInt>,
513
514        /// The number of users with membership of `invite`.
515        #[serde(skip_serializing_if = "Option::is_none")]
516        pub invited_count: Option<UInt>,
517
518        /// The number of timeline events which have just occurred and are not
519        /// historical.
520        #[serde(skip_serializing_if = "Option::is_none")]
521        pub num_live: Option<UInt>,
522
523        /// The bump stamp of the room.
524        ///
525        /// It can be interpreted as a “recency stamp” or “streaming order
526        /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB`
527        /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC`
528        /// receives an update, its `bump_stamp` will be 3.
529        #[serde(skip_serializing_if = "Option::is_none")]
530        pub bump_stamp: Option<UInt>,
531
532        /// Heroes of the room, if requested.
533        #[serde(skip_serializing_if = "Option::is_none")]
534        pub heroes: Option<Vec<Hero>>,
535    }
536
537    impl Room {
538        /// Creates an empty `Room`.
539        pub fn new() -> Self {
540            Default::default()
541        }
542    }
543
544    /// A sliding sync response room hero (see [`Room::heroes`]).
545    #[derive(Clone, Debug, Deserialize, Serialize)]
546    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
547    pub struct Hero {
548        /// The user ID.
549        pub user_id: OwnedUserId,
550
551        /// The name.
552        #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
553        pub name: Option<String>,
554
555        /// The avatar.
556        #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
557        pub avatar: Option<OwnedMxcUri>,
558    }
559
560    impl Hero {
561        /// Creates a new `Hero` with the given user ID.
562        pub fn new(user_id: OwnedUserId) -> Self {
563            Self { user_id, name: None, avatar: None }
564        }
565    }
566
567    /// Extensions responses.
568    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
569    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
570    pub struct Extensions {
571        /// To-device extension response.
572        #[serde(skip_serializing_if = "Option::is_none")]
573        pub to_device: Option<ToDevice>,
574
575        /// E2EE extension response.
576        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
577        pub e2ee: E2EE,
578
579        /// Account data extension response.
580        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
581        pub account_data: AccountData,
582
583        /// Receipts extension response.
584        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
585        pub receipts: Receipts,
586
587        /// Typing extension response.
588        #[serde(default, skip_serializing_if = "Typing::is_empty")]
589        pub typing: Typing,
590    }
591
592    impl Extensions {
593        /// Whether the extension data is empty.
594        ///
595        /// True if neither to-device, e2ee nor account data are to be found.
596        pub fn is_empty(&self) -> bool {
597            self.to_device.is_none()
598                && self.e2ee.is_empty()
599                && self.account_data.is_empty()
600                && self.receipts.is_empty()
601                && self.typing.is_empty()
602        }
603    }
604
605    /// To-device extension response.
606    ///
607    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
608    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
609    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
610    pub struct ToDevice {
611        /// Fetch the next batch from this entry.
612        pub next_batch: String,
613
614        /// The to-device events.
615        #[serde(default, skip_serializing_if = "Vec::is_empty")]
616        pub events: Vec<Raw<AnyToDeviceEvent>>,
617    }
618
619    /// E2EE extension response.
620    ///
621    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
622    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
623    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
624    pub struct E2EE {
625        /// Information on E2EE device updates.
626        #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
627        pub device_lists: DeviceLists,
628
629        /// For each key algorithm, the number of unclaimed one-time keys
630        /// currently held on the server for a device.
631        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
632        pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
633
634        /// The unused fallback key algorithms.
635        ///
636        /// The presence of this field indicates that the server supports
637        /// fallback keys.
638        #[serde(skip_serializing_if = "Option::is_none")]
639        pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
640    }
641
642    impl E2EE {
643        /// Whether all fields are empty or `None`.
644        pub fn is_empty(&self) -> bool {
645            self.device_lists.is_empty()
646                && self.device_one_time_keys_count.is_empty()
647                && self.device_unused_fallback_key_types.is_none()
648        }
649    }
650
651    /// Account-data extension response .
652    ///
653    /// Not yet part of the spec proposal. Taken from the reference implementation
654    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
655    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
656    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
657    pub struct AccountData {
658        /// The global private data created by this user.
659        #[serde(default, skip_serializing_if = "Vec::is_empty")]
660        pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
661
662        /// The private data that this user has attached to each room.
663        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
664        pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
665    }
666
667    impl AccountData {
668        /// Whether all fields are empty or `None`.
669        pub fn is_empty(&self) -> bool {
670            self.global.is_empty() && self.rooms.is_empty()
671        }
672    }
673
674    /// Receipt extension response.
675    ///
676    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
677    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
678    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
679    pub struct Receipts {
680        /// The ephemeral receipt room event for each room.
681        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
682        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
683    }
684
685    impl Receipts {
686        /// Whether all fields are empty or `None`.
687        pub fn is_empty(&self) -> bool {
688            self.rooms.is_empty()
689        }
690    }
691
692    /// Typing extension response.
693    ///
694    /// Not yet part of the spec proposal. Taken from the reference implementation
695    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
696    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
697    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
698    pub struct Typing {
699        /// The ephemeral typing event for each room.
700        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
701        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
702    }
703
704    impl Typing {
705        /// Whether all fields are empty or `None`.
706        pub fn is_empty(&self) -> bool {
707            self.rooms.is_empty()
708        }
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use ruma_common::owned_room_id;
715
716    use super::request::ExtensionRoomConfig;
717
718    #[test]
719    fn serialize_request_extension_room_config() {
720        let entry = ExtensionRoomConfig::AllSubscribed;
721        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
722
723        let entry = ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"));
724        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
725    }
726
727    #[test]
728    fn deserialize_request_extension_room_config() {
729        assert_eq!(
730            serde_json::from_str::<ExtensionRoomConfig>(r#""*""#).unwrap(),
731            ExtensionRoomConfig::AllSubscribed
732        );
733
734        assert_eq!(
735            serde_json::from_str::<ExtensionRoomConfig>(r#""!foo:bar.baz""#).unwrap(),
736            ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"))
737        );
738    }
739}