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(default, skip_serializing_if = "Profiles::is_empty")]
224        pub profiles: Profiles,
225
226        /// Extensions may add further fields to the list.
227        #[serde(flatten)]
228        other: BTreeMap<String, serde_json::Value>,
229    }
230
231    impl Extensions {
232        /// Whether all fields are empty or `None`.
233        pub fn is_empty(&self) -> bool {
234            let mut empty = self.to_device.is_empty()
235                && self.e2ee.is_empty()
236                && self.account_data.is_empty()
237                && self.receipts.is_empty()
238                && self.typing.is_empty()
239                && self.other.is_empty();
240
241            #[cfg(feature = "unstable-msc4308")]
242            {
243                empty = empty && self.thread_subscriptions.is_empty();
244            }
245
246            #[cfg(feature = "unstable-msc4262")]
247            {
248                empty = empty && self.profiles.is_empty();
249            }
250
251            empty
252        }
253    }
254
255    /// Single entry for a room subscription configuration in an extension request.
256    #[derive(Clone, Debug, PartialEq)]
257    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
258    pub enum ExtensionRoomConfig {
259        /// Apply extension to all global room subscriptions.
260        AllSubscribed,
261
262        /// Additionally apply extension to this specific room.
263        Room(OwnedRoomId),
264    }
265
266    impl Serialize for ExtensionRoomConfig {
267        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
268        where
269            S: serde::Serializer,
270        {
271            match self {
272                Self::AllSubscribed => serializer.serialize_str("*"),
273                Self::Room(r) => r.serialize(serializer),
274            }
275        }
276    }
277
278    impl<'de> Deserialize<'de> for ExtensionRoomConfig {
279        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
280        where
281            D: serde::de::Deserializer<'de>,
282        {
283            match deserialize_cow_str(deserializer)?.as_ref() {
284                "*" => Ok(Self::AllSubscribed),
285                other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?)),
286            }
287        }
288    }
289
290    /// To-device messages extension.
291    ///
292    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
293    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
294    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
295    pub struct ToDevice {
296        /// Activate or deactivate this extension.
297        #[serde(skip_serializing_if = "Option::is_none")]
298        pub enabled: Option<bool>,
299
300        /// Maximum number of to-device messages per response.
301        #[serde(skip_serializing_if = "Option::is_none")]
302        pub limit: Option<UInt>,
303
304        /// Give messages since this token only.
305        #[serde(skip_serializing_if = "Option::is_none")]
306        pub since: Option<String>,
307    }
308
309    impl ToDevice {
310        /// Whether all fields are empty or `None`.
311        pub fn is_empty(&self) -> bool {
312            self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
313        }
314    }
315
316    /// E2EE extension configuration.
317    ///
318    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
319    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
320    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
321    pub struct E2EE {
322        /// Activate or deactivate this extension.
323        #[serde(skip_serializing_if = "Option::is_none")]
324        pub enabled: Option<bool>,
325    }
326
327    impl E2EE {
328        /// Whether all fields are empty or `None`.
329        pub fn is_empty(&self) -> bool {
330            self.enabled.is_none()
331        }
332    }
333
334    /// Account-data extension.
335    ///
336    /// Not yet part of the spec proposal. Taken from the reference implementation
337    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
338    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
339    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
340    pub struct AccountData {
341        /// Activate or deactivate this extension.
342        #[serde(skip_serializing_if = "Option::is_none")]
343        pub enabled: Option<bool>,
344
345        /// List of list names for which account data should be enabled.
346        ///
347        /// This is specific to room account data (e.g. user-defined room tags).
348        ///
349        /// If not defined, will be enabled for *all* the lists appearing in the
350        /// request. If defined and empty, will be disabled for all the lists.
351        #[serde(skip_serializing_if = "Option::is_none")]
352        pub lists: Option<Vec<String>>,
353
354        /// List of room names for which account data should be enabled.
355        ///
356        /// This is specific to room account data (e.g. user-defined room tags).
357        ///
358        /// If not defined, will be enabled for *all* the rooms appearing in the
359        /// room subscriptions. If defined and empty, will be disabled for all
360        /// the rooms.
361        #[serde(skip_serializing_if = "Option::is_none")]
362        pub rooms: Option<Vec<ExtensionRoomConfig>>,
363    }
364
365    impl AccountData {
366        /// Whether all fields are empty or `None`.
367        pub fn is_empty(&self) -> bool {
368            self.enabled.is_none()
369        }
370    }
371
372    /// Receipt extension.
373    ///
374    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
375    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
376    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
377    pub struct Receipts {
378        /// Activate or deactivate this extension.
379        #[serde(skip_serializing_if = "Option::is_none")]
380        pub enabled: Option<bool>,
381
382        /// List of list names for which receipts should be enabled.
383        ///
384        /// If not defined, will be enabled for *all* the lists appearing in the
385        /// request. If defined and empty, will be disabled for all the lists.
386        #[serde(skip_serializing_if = "Option::is_none")]
387        pub lists: Option<Vec<String>>,
388
389        /// List of room names for which receipts should be enabled.
390        ///
391        /// If not defined, will be enabled for *all* the rooms appearing in the
392        /// room subscriptions. If defined and empty, will be disabled for all
393        /// the rooms.
394        #[serde(skip_serializing_if = "Option::is_none")]
395        pub rooms: Option<Vec<ExtensionRoomConfig>>,
396    }
397
398    impl Receipts {
399        /// Whether all fields are empty or `None`.
400        pub fn is_empty(&self) -> bool {
401            self.enabled.is_none()
402        }
403    }
404
405    /// Typing extension configuration.
406    ///
407    /// Not yet part of the spec proposal. Taken from the reference implementation
408    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
409    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
410    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
411    pub struct Typing {
412        /// Activate or deactivate this extension.
413        #[serde(skip_serializing_if = "Option::is_none")]
414        pub enabled: Option<bool>,
415
416        /// List of list names for which typing notifications should be enabled.
417        ///
418        /// If not defined, will be enabled for *all* the lists appearing in the
419        /// request. If defined and empty, will be disabled for all the lists.
420        #[serde(skip_serializing_if = "Option::is_none")]
421        pub lists: Option<Vec<String>>,
422
423        /// List of room names for which typing notifications should be enabled.
424        ///
425        /// If not defined, will be enabled for *all* the rooms appearing in the
426        /// room subscriptions. If defined and empty, will be disabled for all
427        /// the rooms.
428        #[serde(skip_serializing_if = "Option::is_none")]
429        pub rooms: Option<Vec<ExtensionRoomConfig>>,
430    }
431
432    impl Typing {
433        /// Whether all fields are empty or `None`.
434        pub fn is_empty(&self) -> bool {
435            self.enabled.is_none()
436        }
437    }
438
439    /// Thread subscriptions extension.
440    ///
441    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
442    #[cfg(feature = "unstable-msc4308")]
443    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
444    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
445    pub struct ThreadSubscriptions {
446        /// Activate or deactivate this extension.
447        #[serde(skip_serializing_if = "Option::is_none")]
448        pub enabled: Option<bool>,
449
450        /// Maximum number of thread subscription changes to receive in the response.
451        ///
452        /// Defaults to 100.
453        /// Servers may impose a smaller limit than what is requested here.
454        #[serde(skip_serializing_if = "Option::is_none")]
455        pub limit: Option<UInt>,
456    }
457
458    #[cfg(feature = "unstable-msc4308")]
459    impl ThreadSubscriptions {
460        /// Whether all fields are empty or `None`.
461        pub fn is_empty(&self) -> bool {
462            self.enabled.is_none() && self.limit.is_none()
463        }
464    }
465
466    /// User profiles extension.
467    ///
468    /// Specified as part of [MSC4262](https://github.com/matrix-org/matrix-spec-proposals/pull/4262).
469    #[cfg(feature = "unstable-msc4262")]
470    #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
471    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
472    pub struct Profiles {
473        /// Activate or deactivate this extension.
474        #[serde(skip_serializing_if = "Option::is_none")]
475        pub enabled: Option<bool>,
476
477        /// List of list names for which user profiles should be enabled.
478        ///
479        /// If not defined, will be enabled for *all* the lists appearing in the
480        /// request. If defined and empty, will be disabled for all the lists.
481        #[serde(skip_serializing_if = "Option::is_none")]
482        pub lists: Option<Vec<String>>,
483
484        /// List of room names for which user profiles should be enabled.
485        ///
486        /// If not defined, will be enabled for *all* the rooms appearing in the
487        /// room subscriptions. If defined and empty, will be disabled for all
488        /// the rooms.
489        #[serde(skip_serializing_if = "Option::is_none")]
490        pub rooms: Option<Vec<ExtensionRoomConfig>>,
491
492        /// Optional filter to control which profile fields to receive updates for. If omitted, all
493        /// profile field updates are included.
494        #[serde(skip_serializing_if = "Option::is_none")]
495        pub fields: Option<Vec<ruma_common::profile::ProfileFieldName>>,
496
497        /// Optional flag to control whether the initial sync includes recent historical profile
498        /// changes:
499        ///
500        /// If false (default), only current profile states are sent on initial sync.
501        /// If true, the server may include recent profile changes that occurred before the sync.
502        #[serde(skip_serializing_if = "Option::is_none")]
503        pub include_history: Option<bool>,
504    }
505
506    #[cfg(feature = "unstable-msc4262")]
507    impl Profiles {
508        /// Whether all fields are empty or `None`.
509        pub fn is_empty(&self) -> bool {
510            self.enabled.is_none()
511        }
512    }
513}
514
515/// Response type for the `/sync` endpoint.
516#[response]
517pub struct Response {
518    /// Matches the `txn_id` sent by the request (see [`Request::txn_id`]).
519    #[serde(skip_serializing_if = "Option::is_none")]
520    pub txn_id: Option<String>,
521
522    /// The token to supply in the `pos` parameter of the next `/sync` request
523    /// (see [`Request::pos`]).
524    pub pos: String,
525
526    /// Resulting details of the lists.
527    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
528    pub lists: BTreeMap<String, response::List>,
529
530    /// The updated rooms.
531    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
532    pub rooms: BTreeMap<OwnedRoomId, response::Room>,
533
534    /// Extensions.
535    #[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
536    pub extensions: response::Extensions,
537}
538
539impl Response {
540    /// Creates a new `Response` with the given `pos`.
541    pub fn new(pos: String) -> Self {
542        Self {
543            txn_id: None,
544            pos,
545            lists: Default::default(),
546            rooms: Default::default(),
547            extensions: Default::default(),
548        }
549    }
550}
551
552/// HTTP types related to a [`Response`].
553pub mod response {
554    use ruma_common::OneTimeKeyAlgorithm;
555    #[cfg(feature = "unstable-msc4308")]
556    use ruma_common::OwnedEventId;
557    #[cfg(feature = "unstable-msc4262")]
558    use ruma_common::profile::UserProfile;
559    use ruma_events::{
560        AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
561        AnyToDeviceEvent, receipt::SyncReceiptEvent, typing::SyncTypingEvent,
562    };
563
564    use super::{
565        super::DeviceLists, AnySyncStateEvent, AnySyncTimelineEvent, BTreeMap, Deserialize,
566        JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize, UInt,
567        UnreadNotificationsCount,
568    };
569    #[cfg(feature = "unstable-msc4308")]
570    use crate::threads::get_thread_subscriptions_changes::unstable::{
571        ThreadSubscription, ThreadUnsubscription,
572    };
573
574    /// A sliding sync response updates to joiend rooms (see
575    /// [`super::Response::lists`]).
576    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
577    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
578    pub struct List {
579        /// The total number of rooms found for this list.
580        pub count: UInt,
581    }
582
583    /// A sliding sync response updated room (see [`super::Response::rooms`]).
584    #[derive(Clone, Debug, Default, Deserialize, Serialize)]
585    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
586    pub struct Room {
587        /// The name as calculated by the server.
588        ///
589        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
590        /// this field is ignored if its deserialization fails.
591        #[serde(skip_serializing_if = "Option::is_none")]
592        #[cfg_attr(
593            feature = "unstable-compat-lax-syncv5-deser",
594            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
595        )]
596        pub name: Option<String>,
597
598        /// The avatar.
599        ///
600        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
601        /// this field is ignored if its deserialization fails.
602        #[serde(default, skip_serializing_if = "JsOption::is_undefined")]
603        #[cfg_attr(
604            feature = "unstable-compat-lax-syncv5-deser",
605            serde(deserialize_with = "ruma_common::serde::default_on_error")
606        )]
607        pub avatar: JsOption<OwnedMxcUri>,
608
609        /// Whether it is an initial response.
610        #[serde(skip_serializing_if = "Option::is_none")]
611        pub initial: Option<bool>,
612
613        /// Whether it is a direct room.
614        #[serde(skip_serializing_if = "Option::is_none")]
615        pub is_dm: Option<bool>,
616
617        /// If this is `Some(_)`, this is a not-yet-accepted invite containing
618        /// the given stripped state events.
619        #[serde(skip_serializing_if = "Option::is_none")]
620        pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
621
622        /// Number of unread notifications.
623        #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
624        pub unread_notifications: UnreadNotificationsCount,
625
626        /// Message-like events and live state events.
627        #[serde(default, skip_serializing_if = "Vec::is_empty")]
628        pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
629
630        /// State events as configured by the request.
631        #[serde(default, skip_serializing_if = "Vec::is_empty")]
632        pub required_state: Vec<Raw<AnySyncStateEvent>>,
633
634        /// The `prev_batch` allowing you to paginate through the messages
635        /// before the given ones.
636        #[serde(skip_serializing_if = "Option::is_none")]
637        pub prev_batch: Option<String>,
638
639        /// True if the number of events returned was limited by the limit on
640        /// the filter.
641        #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
642        pub limited: bool,
643
644        /// The number of users with membership of `join`, including the
645        /// client’s own user ID.
646        #[serde(skip_serializing_if = "Option::is_none")]
647        pub joined_count: Option<UInt>,
648
649        /// The number of users with membership of `invite`.
650        #[serde(skip_serializing_if = "Option::is_none")]
651        pub invited_count: Option<UInt>,
652
653        /// The number of timeline events which have just occurred and are not
654        /// historical.
655        #[serde(skip_serializing_if = "Option::is_none")]
656        pub num_live: Option<UInt>,
657
658        /// The bump stamp of the room.
659        ///
660        /// It can be interpreted as a “recency stamp” or “streaming order
661        /// index”. For example, consider `roomA` with `bump_stamp = 2`, `roomB`
662        /// with `bump_stamp = 1` and `roomC` with `bump_stamp = 0`. If `roomC`
663        /// receives an update, its `bump_stamp` will be 3.
664        #[serde(skip_serializing_if = "Option::is_none")]
665        pub bump_stamp: Option<UInt>,
666
667        /// Heroes of the room.
668        #[serde(skip_serializing_if = "Option::is_none")]
669        pub heroes: Option<Vec<Hero>>,
670    }
671
672    impl Room {
673        /// Creates an empty `Room`.
674        pub fn new() -> Self {
675            Default::default()
676        }
677    }
678
679    /// A sliding sync response room hero (see [`Room::heroes`]).
680    #[derive(Clone, Debug, Deserialize, Serialize)]
681    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
682    pub struct Hero {
683        /// The user ID.
684        pub user_id: OwnedUserId,
685
686        /// The name.
687        ///
688        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
689        /// this field is ignored if its deserialization fails.
690        #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
691        #[cfg_attr(
692            feature = "unstable-compat-lax-syncv5-deser",
693            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
694        )]
695        pub name: Option<String>,
696
697        /// The avatar.
698        ///
699        /// If the `unstable-compat-lax-syncv5-deser` feature is enabled,
700        /// this field is ignored if its deserialization fails.
701        #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
702        #[cfg_attr(
703            feature = "unstable-compat-lax-syncv5-deser",
704            serde(default, deserialize_with = "ruma_common::serde::default_on_error")
705        )]
706        pub avatar: Option<OwnedMxcUri>,
707    }
708
709    impl Hero {
710        /// Creates a new `Hero` with the given user ID.
711        pub fn new(user_id: OwnedUserId) -> Self {
712            Self { user_id, name: None, avatar: None }
713        }
714    }
715
716    /// Extensions responses.
717    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
718    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
719    pub struct Extensions {
720        /// To-device extension response.
721        #[serde(skip_serializing_if = "Option::is_none")]
722        pub to_device: Option<ToDevice>,
723
724        /// E2EE extension response.
725        #[serde(default, skip_serializing_if = "E2EE::is_empty")]
726        pub e2ee: E2EE,
727
728        /// Account data extension response.
729        #[serde(default, skip_serializing_if = "AccountData::is_empty")]
730        pub account_data: AccountData,
731
732        /// Receipts extension response.
733        #[serde(default, skip_serializing_if = "Receipts::is_empty")]
734        pub receipts: Receipts,
735
736        /// Typing extension response.
737        #[serde(default, skip_serializing_if = "Typing::is_empty")]
738        pub typing: Typing,
739
740        /// Thread subscriptions extension response.
741        #[cfg(feature = "unstable-msc4308")]
742        #[serde(
743            default,
744            skip_serializing_if = "ThreadSubscriptions::is_empty",
745            rename = "io.element.msc4308.thread_subscriptions"
746        )]
747        pub thread_subscriptions: ThreadSubscriptions,
748
749        /// Profiles extension response.
750        #[cfg(feature = "unstable-msc4262")]
751        #[serde(default, skip_serializing_if = "BTreeMap::is_empty", rename = "users")]
752        pub profiles: BTreeMap<OwnedUserId, UserProfile>,
753    }
754
755    impl Extensions {
756        /// Whether the extension data is empty.
757        ///
758        /// True if neither to-device, e2ee nor account data are to be found.
759        pub fn is_empty(&self) -> bool {
760            let mut empty = self.to_device.is_none()
761                && self.e2ee.is_empty()
762                && self.account_data.is_empty()
763                && self.receipts.is_empty()
764                && self.typing.is_empty();
765
766            #[cfg(feature = "unstable-msc4308")]
767            {
768                empty = empty && self.thread_subscriptions.is_empty();
769            }
770
771            #[cfg(feature = "unstable-msc4262")]
772            {
773                empty = empty && self.profiles.is_empty();
774            }
775
776            empty
777        }
778    }
779
780    /// To-device extension response.
781    ///
782    /// According to [MSC3885](https://github.com/matrix-org/matrix-spec-proposals/pull/3885).
783    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
784    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
785    pub struct ToDevice {
786        /// Fetch the next batch from this entry.
787        pub next_batch: String,
788
789        /// The to-device events.
790        #[serde(default, skip_serializing_if = "Vec::is_empty")]
791        pub events: Vec<Raw<AnyToDeviceEvent>>,
792    }
793
794    /// E2EE extension response.
795    ///
796    /// According to [MSC3884](https://github.com/matrix-org/matrix-spec-proposals/pull/3884).
797    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
798    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
799    pub struct E2EE {
800        /// Information on E2EE device updates.
801        #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
802        pub device_lists: DeviceLists,
803
804        /// For each key algorithm, the number of unclaimed one-time keys
805        /// currently held on the server for a device.
806        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
807        pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
808
809        /// The unused fallback key algorithms.
810        ///
811        /// The presence of this field indicates that the server supports
812        /// fallback keys.
813        #[serde(skip_serializing_if = "Option::is_none")]
814        pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
815    }
816
817    impl E2EE {
818        /// Whether all fields are empty or `None`.
819        pub fn is_empty(&self) -> bool {
820            self.device_lists.is_empty()
821                && self.device_one_time_keys_count.is_empty()
822                && self.device_unused_fallback_key_types.is_none()
823        }
824    }
825
826    /// Account-data extension response .
827    ///
828    /// Not yet part of the spec proposal. Taken from the reference implementation
829    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/account_data.go>
830    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
831    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
832    pub struct AccountData {
833        /// The global private data created by this user.
834        #[serde(default, skip_serializing_if = "Vec::is_empty")]
835        pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
836
837        /// The private data that this user has attached to each room.
838        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
839        pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
840    }
841
842    impl AccountData {
843        /// Whether all fields are empty or `None`.
844        pub fn is_empty(&self) -> bool {
845            self.global.is_empty() && self.rooms.is_empty()
846        }
847    }
848
849    /// Receipt extension response.
850    ///
851    /// According to [MSC3960](https://github.com/matrix-org/matrix-spec-proposals/pull/3960)
852    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
853    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
854    pub struct Receipts {
855        /// The ephemeral receipt room event for each room.
856        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
857        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
858    }
859
860    impl Receipts {
861        /// Whether all fields are empty or `None`.
862        pub fn is_empty(&self) -> bool {
863            self.rooms.is_empty()
864        }
865    }
866
867    /// Typing extension response.
868    ///
869    /// Not yet part of the spec proposal. Taken from the reference implementation
870    /// <https://github.com/matrix-org/sliding-sync/blob/main/sync3/extensions/typing.go>
871    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
872    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
873    pub struct Typing {
874        /// The ephemeral typing event for each room.
875        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
876        pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
877    }
878
879    impl Typing {
880        /// Whether all fields are empty or `None`.
881        pub fn is_empty(&self) -> bool {
882            self.rooms.is_empty()
883        }
884    }
885
886    /// Thread subscriptions extension response.
887    ///
888    /// Specified as part of [MSC4308](https://github.com/matrix-org/matrix-spec-proposals/pull/4308).
889    #[cfg(feature = "unstable-msc4308")]
890    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
891    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
892    pub struct ThreadSubscriptions {
893        /// New thread subscriptions.
894        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
895        pub subscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadSubscription>>,
896
897        /// New thread unsubscriptions.
898        #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
899        pub unsubscribed: BTreeMap<OwnedRoomId, BTreeMap<OwnedEventId, ThreadUnsubscription>>,
900
901        /// A token that can be used to backpaginate (via the companion endpoint) other thread
902        /// subscription changes that occurred since the last sync, but that were not included in
903        /// this response.
904        ///
905        /// Only set when there are more changes to fetch.
906        #[serde(skip_serializing_if = "Option::is_none")]
907        pub prev_batch: Option<String>,
908    }
909
910    #[cfg(feature = "unstable-msc4308")]
911    impl ThreadSubscriptions {
912        /// Whether all fields are empty or `None`.
913        pub fn is_empty(&self) -> bool {
914            self.subscribed.is_empty() && self.unsubscribed.is_empty() && self.prev_batch.is_none()
915        }
916    }
917}
918
919#[cfg(test)]
920mod tests {
921    use ruma_common::owned_room_id;
922
923    use super::request::ExtensionRoomConfig;
924
925    #[test]
926    fn serialize_request_extension_room_config() {
927        let entry = ExtensionRoomConfig::AllSubscribed;
928        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
929
930        let entry = ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"));
931        assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
932    }
933
934    #[test]
935    fn deserialize_request_extension_room_config() {
936        assert_eq!(
937            serde_json::from_str::<ExtensionRoomConfig>(r#""*""#).unwrap(),
938            ExtensionRoomConfig::AllSubscribed
939        );
940
941        assert_eq!(
942            serde_json::from_str::<ExtensionRoomConfig>(r#""!foo:bar.baz""#).unwrap(),
943            ExtensionRoomConfig::Room(owned_room_id!("!foo:bar.baz"))
944        );
945    }
946
947    #[test]
948    #[cfg(feature = "unstable-compat-lax-syncv5-deser")]
949    fn deserialize_room_ignores_invalid_string_fields() {
950        use super::response::Room;
951
952        let room: Room = serde_json::from_str(
953            r#"{
954                "name": {},
955                "avatar": {},
956                "heroes": [{ "user_id": "@alice:localhost", "displayname": {}, "avatar_url": {} }]
957            }"#,
958        )
959        .unwrap();
960
961        assert_eq!(room.name, None);
962        assert!(room.avatar.is_undefined());
963        let hero = &room.heroes.unwrap()[0];
964        assert_eq!(hero.name, None);
965        assert_eq!(hero.avatar, None);
966
967        // Valid values are still kept.
968        let room: Room = serde_json::from_str(
969            r#"{ "name": "Room", "avatar": "mxc://localhost/a", "heroes": [{ "user_id": "@alice:localhost", "displayname": "Alice", "avatar_url": "mxc://localhost/b" }] }"#,
970        )
971        .unwrap();
972
973        assert_eq!(room.name.as_deref(), Some("Room"));
974        assert_eq!(room.avatar.into_option().unwrap().as_str(), "mxc://localhost/a");
975        let hero = &room.heroes.unwrap()[0];
976        assert_eq!(hero.name.as_deref(), Some("Alice"));
977        assert_eq!(hero.avatar.as_ref().unwrap().as_str(), "mxc://localhost/b");
978    }
979}