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::{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("org.matrix.simplified_msc3575") => "/_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        /// Filters to apply to the list before sorting.
115        #[serde(skip_serializing_if = "Option::is_none")]
116        pub filters: Option<ListFilters>,
117    }
118
119    /// A sliding sync list request filters (see [`List::filters`]).
120    ///
121    /// All fields are applied with _AND_ operators. The absence of fields
122    /// implies no filter on that criteria: it does NOT imply `false`.
123    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
124    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
125    pub struct ListFilters {
126        /// Whether to return only DM rooms (as determined by the `m.direct` account data event),
127        /// only non-DM rooms, or both.
128        #[serde(skip_serializing_if = "Option::is_none")]
129        pub is_dm: Option<bool>,
130
131        /// Whether to return only encrypted rooms (as determined by the existence of an
132        /// `m.room.encryption` state event), only unencrypted rooms, or both.
133        #[serde(skip_serializing_if = "Option::is_none")]
134        pub is_encrypted: Option<bool>,
135
136        /// Whether to return only invited rooms, only joined rooms, or both.
137        #[serde(skip_serializing_if = "Option::is_none")]
138        pub is_invite: Option<bool>,
139
140        /// Only list rooms with these create-types, or all.
141        ///
142        /// If a room type is specified in both `room_types` and `not_room_types`,
143        /// `not_room_types` wins and the corresponding rooms are not included.
144        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
145        pub room_types: Vec<RoomTypeFilter>,
146
147        /// Only list rooms that are not of these create-types, or all.
148        ///
149        /// If a room type is specified in both `room_types` and `not_room_types`,
150        /// `not_room_types` wins and the corresponding rooms are not included.
151        #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
152        pub not_room_types: Vec<RoomTypeFilter>,
153    }
154
155    /// Sliding sync request room subscription (see [`super::Request::room_subscriptions`]).
156    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
157    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
158    pub struct RoomSubscription {
159        /// Required state for each returned room. An array of event type and
160        /// state key tuples.
161        #[serde(default, skip_serializing_if = "Vec::is_empty")]
162        pub required_state: Vec<(StateEventType, String)>,
163
164        /// The maximum number of timeline events to return per room.
165        pub timeline_limit: UInt,
166    }
167
168    /// Sliding sync request room details (see [`List::room_details`]).
169    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
170    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
171    pub struct RoomDetails {
172        /// Required state for each returned room. An array of event type and state key tuples.
173        #[serde(default, skip_serializing_if = "Vec::is_empty")]
174        pub required_state: Vec<(StateEventType, String)>,
175
176        /// The maximum number of timeline events to return per room.
177        pub timeline_limit: UInt,
178    }
179
180    /// Sliding sync request extensions (see [`super::Request::extensions`]).
181    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
182    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
183    pub struct Extensions {
184        /// Configure the to-device extension.
185        #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
186        pub to_device: ToDevice,
187
188        /// Configure the E2EE extension.
189        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
190        pub e2ee: E2EE,
191
192        /// Configure the account data extension.
193        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
194        pub account_data: AccountData,
195
196        /// Configure the receipts extension.
197        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
198        pub receipts: Receipts,
199
200        /// Configure the typing extension.
201        #[serde(default, skip_serializing_if = "Typing::is_empty")]
202        pub typing: Typing,
203
204        /// Configure the thread subscriptions extension.
205        #[cfg(feature = "unstable-msc4308")]
206        #[serde(
207            default,
208            skip_serializing_if = "ThreadSubscriptions::is_empty",
209            rename = "io.element.msc4308.thread_subscriptions"
210        )]
211        pub thread_subscriptions: ThreadSubscriptions,
212
213        /// Extensions may add further fields to the list.
214        #[serde(flatten)]
215        other: BTreeMap<String, serde_json::Value>,
216    }
217
218    impl Extensions {
219        /// Whether all fields are empty or `None`.
220        pub fn is_empty(&self) -> bool {
221            self.to_device.is_empty()
222                && self.e2ee.is_empty()
223                && self.account_data.is_empty()
224                && self.receipts.is_empty()
225                && self.typing.is_empty()
226                && self.other.is_empty()
227        }
228    }
229
230    /// Single entry for a room subscription configuration in an extension request.
231    #[derive(Clone, Debug, PartialEq)]
232    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
233    pub enum ExtensionRoomConfig {
234        /// Apply extension to all global room subscriptions.
235        AllSubscribed,
236
237        /// Additionally apply extension to this specific room.
238        Room(OwnedRoomId),
239    }
240
241    impl Serialize for ExtensionRoomConfig {
242        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
243        where
244            S: serde::Serializer,
245        {
246            match self {
247                Self::AllSubscribed => serializer.serialize_str("*"),
248                Self::Room(r) => r.serialize(serializer),
249            }
250        }
251    }
252
253    impl<'de> Deserialize<'de> for ExtensionRoomConfig {
254        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
255        where
256            D: serde::de::Deserializer<'de>,
257        {
258            match deserialize_cow_str(deserializer)?.as_ref() {
259                "*" => Ok(Self::AllSubscribed),
260                other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())),
261            }
262        }
263    }
264
265    /// To-device messages extension.
266    ///
267    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
268    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
269    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
270    pub struct ToDevice {
271        /// Activate or deactivate this extension.
272        #[serde(skip_serializing_if = "Option::is_none")]
273        pub enabled: Option<bool>,
274
275        /// Maximum number of to-device messages per response.
276        #[serde(skip_serializing_if = "Option::is_none")]
277        pub limit: Option<UInt>,
278
279        /// Give messages since this token only.
280        #[serde(skip_serializing_if = "Option::is_none")]
281        pub since: Option<String>,
282    }
283
284    impl ToDevice {
285        /// Whether all fields are empty or `None`.
286        pub fn is_empty(&self) -> bool {
287            self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
288        }
289    }
290
291    /// E2EE extension configuration.
292    ///
293    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
294    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
295    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
296    pub struct E2EE {
297        /// Activate or deactivate this extension.
298        #[serde(skip_serializing_if = "Option::is_none")]
299        pub enabled: Option<bool>,
300    }
301
302    impl E2EE {
303        /// Whether all fields are empty or `None`.
304        pub fn is_empty(&self) -> bool {
305            self.enabled.is_none()
306        }
307    }
308
309    /// Account-data extension .
310    ///
311    /// Not yet part of the spec proposal. Taken from the reference implementation
312    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
313    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
314    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
315    pub struct AccountData {
316        /// Activate or deactivate this extension.
317        #[serde(skip_serializing_if = "Option::is_none")]
318        pub enabled: Option<bool>,
319
320        /// List of list names for which account data should be enabled.
321        ///
322        /// This is specific to room account data (e.g. user-defined room tags).
323        ///
324        /// If not defined, will be enabled for *all* the lists appearing in the
325        /// request. If defined and empty, will be disabled for all the lists.
326        #[serde(skip_serializing_if = "Option::is_none")]
327        pub lists: Option<Vec<String>>,
328
329        /// List of room names for which account data should be enabled.
330        ///
331        /// This is specific to room account data (e.g. user-defined room tags).
332        ///
333        /// If not defined, will be enabled for *all* the rooms appearing in the
334        /// room subscriptions. If defined and empty, will be disabled for all
335        /// the rooms.
336        #[serde(skip_serializing_if = "Option::is_none")]
337        pub rooms: Option<Vec<ExtensionRoomConfig>>,
338    }
339
340    impl AccountData {
341        /// Whether all fields are empty or `None`.
342        pub fn is_empty(&self) -> bool {
343            self.enabled.is_none()
344        }
345    }
346
347    /// Receipt extension.
348    ///
349    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
350    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
351    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
352    pub struct Receipts {
353        /// Activate or deactivate this extension.
354        #[serde(skip_serializing_if = "Option::is_none")]
355        pub enabled: Option<bool>,
356
357        /// List of list names for which receipts should be enabled.
358        ///
359        /// If not defined, will be enabled for *all* the lists appearing in the
360        /// request. If defined and empty, will be disabled for all the lists.
361        #[serde(skip_serializing_if = "Option::is_none")]
362        pub lists: Option<Vec<String>>,
363
364        /// List of room names for which receipts should be enabled.
365        ///
366        /// If not defined, will be enabled for *all* the rooms appearing in the
367        /// room subscriptions. If defined and empty, will be disabled for all
368        /// the rooms.
369        #[serde(skip_serializing_if = "Option::is_none")]
370        pub rooms: Option<Vec<ExtensionRoomConfig>>,
371    }
372
373    impl Receipts {
374        /// Whether all fields are empty or `None`.
375        pub fn is_empty(&self) -> bool {
376            self.enabled.is_none()
377        }
378    }
379
380    /// Typing extension configuration.
381    ///
382    /// Not yet part of the spec proposal. Taken from the reference implementation
383    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
384    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
385    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
386    pub struct Typing {
387        /// Activate or deactivate this extension.
388        #[serde(skip_serializing_if = "Option::is_none")]
389        pub enabled: Option<bool>,
390
391        /// List of list names for which typing notifications should be enabled.
392        ///
393        /// If not defined, will be enabled for *all* the lists appearing in the
394        /// request. If defined and empty, will be disabled for all the lists.
395        #[serde(skip_serializing_if = "Option::is_none")]
396        pub lists: Option<Vec<String>>,
397
398        /// List of room names for which typing notifications should be enabled.
399        ///
400        /// If not defined, will be enabled for *all* the rooms appearing in the
401        /// room subscriptions. If defined and empty, will be disabled for all
402        /// the rooms.
403        #[serde(skip_serializing_if = "Option::is_none")]
404        pub rooms: Option<Vec<ExtensionRoomConfig>>,
405    }
406
407    impl Typing {
408        /// Whether all fields are empty or `None`.
409        pub fn is_empty(&self) -> bool {
410            self.enabled.is_none()
411        }
412    }
413
414    /// Thread subscriptions extension.
415    ///
416    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
417    #[cfg(feature = "unstable-msc4308")]
418    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
419    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
420    pub struct ThreadSubscriptions {
421        /// Activate or deactivate this extension.
422        #[serde(skip_serializing_if = "Option::is_none")]
423        pub enabled: Option<bool>,
424
425        /// Maximum number of thread subscription changes to receive in the response.
426        ///
427        /// Defaults to 100.
428        /// Servers may impose a smaller limit than what is requested here.
429        #[serde(skip_serializing_if = "Option::is_none")]
430        pub limit: Option<UInt>,
431    }
432
433    #[cfg(feature = "unstable-msc4308")]
434    impl ThreadSubscriptions {
435        /// Whether all fields are empty or `None`.
436        pub fn is_empty(&self) -> bool {
437            self.enabled.is_none() && self.limit.is_none()
438        }
439    }
440}
441
442/// Response type for the `/sync` endpoint.
443#[response(error = crate::Error)]
444pub struct Response {
445    /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]).
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub txn_id: Option<String>,
448
449    /// The token to supply in the `pos` parameter of the next `/sync` request
450    /// (see [`Request::pos`]).
451    pub pos: String,
452
453    /// Resulting details of the lists.
454    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
455    pub lists: BTreeMap<String, response::List>,
456
457    /// The updated rooms.
458    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
459    pub rooms: BTreeMap<OwnedRoomId, response::Room>,
460
461    /// Extensions.
462    #[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
463    pub extensions: response::Extensions,
464}
465
466impl Response {
467    /// Creates a new `Response` with the given `pos`.
468    pub fn new(pos: String) -> Self {
469        Self {
470            txn_id: None,
471            pos,
472            lists: Default::default(),
473            rooms: Default::default(),
474            extensions: Default::default(),
475        }
476    }
477}
478
479/// HTTP types related to a [`Response`].
480pub mod response {
481    use ruma_common::OneTimeKeyAlgorithm;
482    #[cfg(feature = "unstable-msc4308")]
483    use ruma_common::OwnedEventId;
484    use ruma_events::{
485        receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent,
486        AnyRoomAccountDataEvent, AnyStrippedStateEvent, AnyToDeviceEvent,
487    };
488
489    use super::{
490        super::DeviceLists, AnySyncStateEvent, AnySyncTimelineEvent, BTreeMap, Deserialize,
491        JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, UInt,
492        UnreadNotificationsCount,
493    };
494    #[cfg(feature = "unstable-msc4308")]
495    use crate::threads::get_thread_subscriptions_changes::unstable::{
496        ThreadSubscription, ThreadUnsubscription,
497    };
498
499    /// A sliding sync response updates to joiend rooms (see
500    /// [`super::Response::lists`]).
501    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
502    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
503    pub struct List {
504        /// The total number of rooms found for this list.
505        pub count: UInt,
506    }
507
508    /// A sliding sync response updated room (see [`super::Response::rooms`]).
509    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
510    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
511    pub struct Room {
512        /// The name as calculated by the server.
513        #[serde(skip_serializing_if = "Option::is_none")]
514        pub name: Option<String>,
515
516        /// The avatar.
517        #[serde(default, skip_serializing_if = "JsOption::is_undefined")]
518        pub avatar: JsOption<OwnedMxcUri>,
519
520        /// Whether it is an initial response.
521        #[serde(skip_serializing_if = "Option::is_none")]
522        pub initial: Option<bool>,
523
524        /// Whether it is a direct room.
525        #[serde(skip_serializing_if = "Option::is_none")]
526        pub is_dm: Option<bool>,
527
528        /// If this is `Some(_)`, this is a not-yet-accepted invite containing
529        /// the given stripped state events.
530        #[serde(skip_serializing_if = "Option::is_none")]
531        pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
532
533        /// Number of unread notifications.
534        #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
535        pub unread_notifications: UnreadNotificationsCount,
536
537        /// Message-like events and live state events.
538        #[serde(default, skip_serializing_if = "Vec::is_empty")]
539        pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
540
541        /// State events as configured by the request.
542        #[serde(default, skip_serializing_if = "Vec::is_empty")]
543        pub required_state: Vec<Raw<AnySyncStateEvent>>,
544
545        /// The `prev_batch` allowing you to paginate through the messages
546        /// before the given ones.
547        #[serde(skip_serializing_if = "Option::is_none")]
548        pub prev_batch: Option<String>,
549
550        /// True if the number of events returned was limited by the limit on
551        /// the filter.
552        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
553        pub limited: bool,
554
555        /// The number of users with membership of `join`, including the
556        /// client’s own user ID.
557        #[serde(skip_serializing_if = "Option::is_none")]
558        pub joined_count: Option<UInt>,
559
560        /// The number of users with membership of `invite`.
561        #[serde(skip_serializing_if = "Option::is_none")]
562        pub invited_count: Option<UInt>,
563
564        /// The number of timeline events which have just occurred and are not
565        /// historical.
566        #[serde(skip_serializing_if = "Option::is_none")]
567        pub num_live: Option<UInt>,
568
569        /// The bump stamp of the room.
570        ///
571        /// It can be interpreted as a “recency stamp” or “streaming order
572        /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB`
573        /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC`
574        /// receives an update, its `bump_stamp` will be 3.
575        #[serde(skip_serializing_if = "Option::is_none")]
576        pub bump_stamp: Option<UInt>,
577
578        /// Heroes of the room.
579        #[serde(skip_serializing_if = "Option::is_none")]
580        pub heroes: Option<Vec<Hero>>,
581    }
582
583    impl Room {
584        /// Creates an empty `Room`.
585        pub fn new() -> Self {
586            Default::default()
587        }
588    }
589
590    /// A sliding sync response room hero (see [`Room::heroes`]).
591    #[derive(Clone, Debug, Deserialize, Serialize)]
592    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
593    pub struct Hero {
594        /// The user ID.
595        pub user_id: OwnedUserId,
596
597        /// The name.
598        #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
599        pub name: Option<String>,
600
601        /// The avatar.
602        #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
603        pub avatar: Option<OwnedMxcUri>,
604    }
605
606    impl Hero {
607        /// Creates a new `Hero` with the given user ID.
608        pub fn new(user_id: OwnedUserId) -> Self {
609            Self { user_id, name: None, avatar: None }
610        }
611    }
612
613    /// Extensions responses.
614    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
615    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
616    pub struct Extensions {
617        /// To-device extension response.
618        #[serde(skip_serializing_if = "Option::is_none")]
619        pub to_device: Option<ToDevice>,
620
621        /// E2EE extension response.
622        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
623        pub e2ee: E2EE,
624
625        /// Account data extension response.
626        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
627        pub account_data: AccountData,
628
629        /// Receipts extension response.
630        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
631        pub receipts: Receipts,
632
633        /// Typing extension response.
634        #[serde(default, skip_serializing_if = "Typing::is_empty")]
635        pub typing: Typing,
636
637        /// Thread subscriptions extension response.
638        #[cfg(feature = "unstable-msc4308")]
639        #[serde(
640            default,
641            skip_serializing_if = "ThreadSubscriptions::is_empty",
642            rename = "io.element.msc4308.thread_subscriptions"
643        )]
644        pub thread_subscriptions: ThreadSubscriptions,
645    }
646
647    impl Extensions {
648        /// Whether the extension data is empty.
649        ///
650        /// True if neither to-device, e2ee nor account data are to be found.
651        pub fn is_empty(&self) -> bool {
652            self.to_device.is_none()
653                && self.e2ee.is_empty()
654                && self.account_data.is_empty()
655                && self.receipts.is_empty()
656                && self.typing.is_empty()
657        }
658    }
659
660    /// To-device extension response.
661    ///
662    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
663    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
664    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
665    pub struct ToDevice {
666        /// Fetch the next batch from this entry.
667        pub next_batch: String,
668
669        /// The to-device events.
670        #[serde(default, skip_serializing_if = "Vec::is_empty")]
671        pub events: Vec<Raw<AnyToDeviceEvent>>,
672    }
673
674    /// E2EE extension response.
675    ///
676    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
677    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
678    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
679    pub struct E2EE {
680        /// Information on E2EE device updates.
681        #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
682        pub device_lists: DeviceLists,
683
684        /// For each key algorithm, the number of unclaimed one-time keys
685        /// currently held on the server for a device.
686        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
687        pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
688
689        /// The unused fallback key algorithms.
690        ///
691        /// The presence of this field indicates that the server supports
692        /// fallback keys.
693        #[serde(skip_serializing_if = "Option::is_none")]
694        pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
695    }
696
697    impl E2EE {
698        /// Whether all fields are empty or `None`.
699        pub fn is_empty(&self) -> bool {
700            self.device_lists.is_empty()
701                && self.device_one_time_keys_count.is_empty()
702                && self.device_unused_fallback_key_types.is_none()
703        }
704    }
705
706    /// Account-data extension response .
707    ///
708    /// Not yet part of the spec proposal. Taken from the reference implementation
709    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
710    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
711    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
712    pub struct AccountData {
713        /// The global private data created by this user.
714        #[serde(default, skip_serializing_if = "Vec::is_empty")]
715        pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
716
717        /// The private data that this user has attached to each room.
718        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
719        pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
720    }
721
722    impl AccountData {
723        /// Whether all fields are empty or `None`.
724        pub fn is_empty(&self) -> bool {
725            self.global.is_empty() && self.rooms.is_empty()
726        }
727    }
728
729    /// Receipt extension response.
730    ///
731    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
732    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
733    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
734    pub struct Receipts {
735        /// The ephemeral receipt room event for each room.
736        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
737        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
738    }
739
740    impl Receipts {
741        /// Whether all fields are empty or `None`.
742        pub fn is_empty(&self) -> bool {
743            self.rooms.is_empty()
744        }
745    }
746
747    /// Typing extension response.
748    ///
749    /// Not yet part of the spec proposal. Taken from the reference implementation
750    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
751    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
752    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
753    pub struct Typing {
754        /// The ephemeral typing event for each room.
755        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
756        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
757    }
758
759    impl Typing {
760        /// Whether all fields are empty or `None`.
761        pub fn is_empty(&self) -> bool {
762            self.rooms.is_empty()
763        }
764    }
765
766    /// Thread subscriptions extension response.
767    ///
768    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
769    #[cfg(feature = "unstable-msc4308")]
770    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
771    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
772    pub struct ThreadSubscriptions {
773        /// New thread subscriptions.
774        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
775        pub subscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadSubscription>>,
776
777        /// New thread unsubscriptions.
778        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
779        pub unsubscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadUnsubscription>>,
780
781        /// A token that can be used to backpaginate (via the companion endpoint) other thread
782        /// subscription changes that occurred since the last sync, but that were not included in
783        /// this response.
784        ///
785        /// Only set when there are more changes to fetch.
786        #[serde(skip_serializing_if = "Option::is_none")]
787        pub prev_batch: Option<String>,
788    }
789
790    #[cfg(feature = "unstable-msc4308")]
791    impl ThreadSubscriptions {
792        /// Whether all fields are empty or `None`.
793        pub fn is_empty(&self) -> bool {
794            self.subscribed.is_empty() && self.unsubscribed.is_empty() && self.prev_batch.is_none()
795        }
796    }
797}
798
799#[cfg(test)]
800mod tests {
801    use ruma_common::owned_room_id;
802
803    use super::request::ExtensionRoomConfig;
804
805    #[test]
806    fn serialize_request_extension_room_config() {
807        let entry = ExtensionRoomConfig::AllSubscribed;
808        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
809
810        let entry = ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"));
811        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
812    }
813
814    #[test]
815    fn deserialize_request_extension_room_config() {
816        assert_eq!(
817            serde_json::from_str::<ExtensionRoomConfig>(r#""*""#).unwrap(),
818            ExtensionRoomConfig::AllSubscribed
819        );
820
821        assert_eq!(
822            serde_json::from_str::<ExtensionRoomConfig>(r#""!foo:bar.baz""#).unwrap(),
823            ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"))
824        );
825    }
826}