Skip to main content

ruma_client_api/sync/sync_events/
v5.rs

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