ruma_client_api/sync/sync_events/
v5.rs

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