ruma_client_api/sync/sync_events/
v3.rs

1//! `/v3/` ([spec])
2//!
3//! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3sync
4
5use std::{collections::BTreeMap, time::Duration};
6
7use as_variant::as_variant;
8use js_int::UInt;
9use ruma_common::{
10    api::{request, response, Metadata},
11    metadata,
12    presence::PresenceState,
13    serde::Raw,
14    OneTimeKeyAlgorithm, OwnedEventId, OwnedRoomId, OwnedUserId,
15};
16use ruma_events::{
17    presence::PresenceEvent, AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent,
18    AnyStrippedStateEvent, AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnySyncTimelineEvent,
19    AnyToDeviceEvent,
20};
21use serde::{Deserialize, Serialize};
22
23mod response_serde;
24
25use super::{DeviceLists, UnreadNotificationsCount};
26use crate::filter::FilterDefinition;
27
28const METADATA: Metadata = metadata! {
29    method: GET,
30    rate_limited: false,
31    authentication: AccessToken,
32    history: {
33        1.0 => "/_matrix/client/r0/sync",
34        1.1 => "/_matrix/client/v3/sync",
35    }
36};
37
38/// Request type for the `sync` endpoint.
39#[request(error = crate::Error)]
40#[derive(Default)]
41pub struct Request {
42    /// A filter represented either as its full JSON definition or the ID of a saved filter.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    #[ruma_api(query)]
45    pub filter: Option<Filter>,
46
47    /// A point in time to continue a sync from.
48    ///
49    /// Should be a token from the `next_batch` field of a previous `/sync`
50    /// request.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    #[ruma_api(query)]
53    pub since: Option<String>,
54
55    /// Controls whether to include the full state for all rooms the user is a member of.
56    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
57    #[ruma_api(query)]
58    pub full_state: bool,
59
60    /// Controls whether the client is automatically marked as online by polling this API.
61    ///
62    /// Defaults to `PresenceState::Online`.
63    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
64    #[ruma_api(query)]
65    pub set_presence: PresenceState,
66
67    /// The maximum time to poll in milliseconds before returning this request.
68    #[serde(
69        with = "ruma_common::serde::duration::opt_ms",
70        default,
71        skip_serializing_if = "Option::is_none"
72    )]
73    #[ruma_api(query)]
74    pub timeout: Option<Duration>,
75
76    /// Controls whether to receive state changes between the previous sync and the **start** of
77    /// the timeline, or between the previous sync and the **end** of the timeline.
78    #[cfg(feature = "unstable-msc4222")]
79    #[serde(
80        default,
81        skip_serializing_if = "ruma_common::serde::is_default",
82        rename = "org.matrix.msc4222.use_state_after"
83    )]
84    #[ruma_api(query)]
85    pub use_state_after: bool,
86}
87
88/// Response type for the `sync` endpoint.
89#[response(error = crate::Error)]
90pub struct Response {
91    /// The batch token to supply in the `since` param of the next `/sync` request.
92    pub next_batch: String,
93
94    /// Updates to rooms.
95    #[serde(default, skip_serializing_if = "Rooms::is_empty")]
96    pub rooms: Rooms,
97
98    /// Updates to the presence status of other users.
99    #[serde(default, skip_serializing_if = "Presence::is_empty")]
100    pub presence: Presence,
101
102    /// The global private data created by this user.
103    #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
104    pub account_data: GlobalAccountData,
105
106    /// Messages sent directly between devices.
107    #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
108    pub to_device: ToDevice,
109
110    /// Information on E2E device updates.
111    ///
112    /// Only present on an incremental sync.
113    #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
114    pub device_lists: DeviceLists,
115
116    /// For each key algorithm, the number of unclaimed one-time keys
117    /// currently held on the server for a device.
118    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
119    pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
120
121    /// The unused fallback key algorithms.
122    ///
123    /// The presence of this field indicates that the server supports
124    /// fallback keys.
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
127}
128
129impl Request {
130    /// Creates an empty `Request`.
131    pub fn new() -> Self {
132        Default::default()
133    }
134}
135
136impl Response {
137    /// Creates a new `Response` with the given batch token.
138    pub fn new(next_batch: String) -> Self {
139        Self {
140            next_batch,
141            rooms: Default::default(),
142            presence: Default::default(),
143            account_data: Default::default(),
144            to_device: Default::default(),
145            device_lists: Default::default(),
146            device_one_time_keys_count: BTreeMap::new(),
147            device_unused_fallback_key_types: None,
148        }
149    }
150}
151
152/// A filter represented either as its full JSON definition or the ID of a saved filter.
153#[derive(Clone, Debug, Deserialize, Serialize)]
154#[allow(clippy::large_enum_variant)]
155#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
156#[serde(untagged)]
157pub enum Filter {
158    // The filter definition needs to be (de)serialized twice because it is a URL-encoded JSON
159    // string. Since #[ruma_api(query)] only does the latter and this is a very uncommon
160    // setup, we implement it through custom serde logic for this specific enum variant rather
161    // than adding another ruma_api attribute.
162    //
163    // On the deserialization side, because this is an enum with #[serde(untagged)], serde
164    // will try the variants in order (https://serde.rs/enum-representations.html). That means because
165    // FilterDefinition is the first variant, JSON decoding is attempted first which is almost
166    // functionally equivalent to looking at whether the first symbol is a '{' as the spec
167    // says. (there are probably some corner cases like leading whitespace)
168    /// A complete filter definition serialized to JSON.
169    #[serde(with = "ruma_common::serde::json_string")]
170    FilterDefinition(FilterDefinition),
171
172    /// The ID of a filter saved on the server.
173    FilterId(String),
174}
175
176impl From<FilterDefinition> for Filter {
177    fn from(def: FilterDefinition) -> Self {
178        Self::FilterDefinition(def)
179    }
180}
181
182impl From<String> for Filter {
183    fn from(id: String) -> Self {
184        Self::FilterId(id)
185    }
186}
187
188/// Updates to rooms.
189#[derive(Clone, Debug, Default, Deserialize, Serialize)]
190#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
191pub struct Rooms {
192    /// The rooms that the user has left or been banned from.
193    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
194    pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
195
196    /// The rooms that the user has joined.
197    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
198    pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
199
200    /// The rooms that the user has been invited to.
201    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
202    pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
203
204    /// The rooms that the user has knocked on.
205    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
206    pub knock: BTreeMap<OwnedRoomId, KnockedRoom>,
207}
208
209impl Rooms {
210    /// Creates an empty `Rooms`.
211    pub fn new() -> Self {
212        Default::default()
213    }
214
215    /// Returns true if there is no update in any room.
216    pub fn is_empty(&self) -> bool {
217        self.leave.is_empty() && self.join.is_empty() && self.invite.is_empty()
218    }
219}
220
221/// Historical updates to left rooms.
222#[derive(Clone, Debug, Default, Serialize)]
223#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
224pub struct LeftRoom {
225    /// The timeline of messages and state changes in the room up to the point when the user
226    /// left.
227    #[serde(skip_serializing_if = "Timeline::is_empty")]
228    pub timeline: Timeline,
229
230    /// The state updates for the room up to the start of the timeline.
231    #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
232    pub state: State,
233
234    /// The private data that this user has attached to this room.
235    #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
236    pub account_data: RoomAccountData,
237}
238
239impl LeftRoom {
240    /// Creates an empty `LeftRoom`.
241    pub fn new() -> Self {
242        Default::default()
243    }
244
245    /// Returns true if there are updates in the room.
246    pub fn is_empty(&self) -> bool {
247        self.timeline.is_empty() && self.state.is_empty() && self.account_data.is_empty()
248    }
249}
250
251/// Updates to joined rooms.
252#[derive(Clone, Debug, Default, Serialize)]
253#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
254pub struct JoinedRoom {
255    /// Information about the room which clients may need to correctly render it
256    /// to users.
257    #[serde(skip_serializing_if = "RoomSummary::is_empty")]
258    pub summary: RoomSummary,
259
260    /// Counts of [unread notifications] for this room.
261    ///
262    /// If `unread_thread_notifications` was set to `true` in the [`RoomEventFilter`], these
263    /// include only the unread notifications for the main timeline.
264    ///
265    /// [unread notifications]: https://spec.matrix.org/latest/client-server-api/#receiving-notifications
266    /// [`RoomEventFilter`]: crate::filter::RoomEventFilter
267    #[serde(skip_serializing_if = "UnreadNotificationsCount::is_empty")]
268    pub unread_notifications: UnreadNotificationsCount,
269
270    /// Counts of [unread notifications] for threads in this room.
271    ///
272    /// This is a map from thread root ID to unread notifications in the thread.
273    ///
274    /// Only set if `unread_thread_notifications` was set to `true` in the [`RoomEventFilter`].
275    ///
276    /// [unread notifications]: https://spec.matrix.org/latest/client-server-api/#receiving-notifications
277    /// [`RoomEventFilter`]: crate::filter::RoomEventFilter
278    #[serde(skip_serializing_if = "BTreeMap::is_empty")]
279    pub unread_thread_notifications: BTreeMap<OwnedEventId, UnreadNotificationsCount>,
280
281    /// The timeline of messages and state changes in the room.
282    #[serde(skip_serializing_if = "Timeline::is_empty")]
283    pub timeline: Timeline,
284
285    /// Updates to the state, between the time indicated by the `since` parameter, and the
286    /// start of the `timeline` (or all state up to the start of the `timeline`, if
287    /// `since` is not given, or `full_state` is true).
288    #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
289    pub state: State,
290
291    /// The private data that this user has attached to this room.
292    #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
293    pub account_data: RoomAccountData,
294
295    /// The ephemeral events in the room that aren't recorded in the timeline or state of the
296    /// room.
297    #[serde(skip_serializing_if = "Ephemeral::is_empty")]
298    pub ephemeral: Ephemeral,
299
300    /// The number of unread events since the latest read receipt.
301    ///
302    /// This uses the unstable prefix in [MSC2654].
303    ///
304    /// [MSC2654]: https://github.com/matrix-org/matrix-spec-proposals/pull/2654
305    #[cfg(feature = "unstable-msc2654")]
306    #[serde(rename = "org.matrix.msc2654.unread_count", skip_serializing_if = "Option::is_none")]
307    pub unread_count: Option<UInt>,
308}
309
310impl JoinedRoom {
311    /// Creates an empty `JoinedRoom`.
312    pub fn new() -> Self {
313        Default::default()
314    }
315
316    /// Returns true if there are no updates in the room.
317    pub fn is_empty(&self) -> bool {
318        let is_empty = self.summary.is_empty()
319            && self.unread_notifications.is_empty()
320            && self.unread_thread_notifications.is_empty()
321            && self.timeline.is_empty()
322            && self.state.is_empty()
323            && self.account_data.is_empty()
324            && self.ephemeral.is_empty();
325
326        #[cfg(not(feature = "unstable-msc2654"))]
327        return is_empty;
328
329        #[cfg(feature = "unstable-msc2654")]
330        return is_empty && self.unread_count.is_none();
331    }
332}
333
334/// Updates to a room that the user has knocked upon.
335#[derive(Clone, Debug, Default, Deserialize, Serialize)]
336#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
337pub struct KnockedRoom {
338    /// Updates to the stripped state of the room.
339    #[serde(default, skip_serializing_if = "KnockState::is_empty")]
340    pub knock_state: KnockState,
341}
342
343impl KnockedRoom {
344    /// Creates an empty `KnockedRoom`.
345    pub fn new() -> Self {
346        Default::default()
347    }
348
349    /// Whether there are updates for this room.
350    pub fn is_empty(&self) -> bool {
351        self.knock_state.is_empty()
352    }
353}
354
355impl From<KnockState> for KnockedRoom {
356    fn from(knock_state: KnockState) -> Self {
357        KnockedRoom { knock_state, ..Default::default() }
358    }
359}
360
361/// Stripped state updates of a room that the user has knocked upon.
362#[derive(Clone, Debug, Default, Deserialize, Serialize)]
363#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
364pub struct KnockState {
365    /// The stripped state of a room that the user has knocked upon.
366    #[serde(default, skip_serializing_if = "Vec::is_empty")]
367    pub events: Vec<Raw<AnyStrippedStateEvent>>,
368}
369
370impl KnockState {
371    /// Creates an empty `KnockState`.
372    pub fn new() -> Self {
373        Default::default()
374    }
375
376    /// Whether there are stripped state updates in this room.
377    pub fn is_empty(&self) -> bool {
378        self.events.is_empty()
379    }
380}
381
382/// Events in the room.
383#[derive(Clone, Debug, Default, Deserialize, Serialize)]
384#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
385pub struct Timeline {
386    /// True if the number of events returned was limited by the `limit` on the filter.
387    ///
388    /// Default to `false`.
389    #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
390    pub limited: bool,
391
392    /// A token that can be supplied to to the `from` parameter of the
393    /// `/rooms/{roomId}/messages` endpoint.
394    #[serde(skip_serializing_if = "Option::is_none")]
395    pub prev_batch: Option<String>,
396
397    /// A list of events.
398    pub events: Vec<Raw<AnySyncTimelineEvent>>,
399}
400
401impl Timeline {
402    /// Creates an empty `Timeline`.
403    pub fn new() -> Self {
404        Default::default()
405    }
406
407    /// Returns true if there are no timeline updates.
408    ///
409    /// A `Timeline` is considered non-empty if it has at least one event, a
410    /// `prev_batch` value, or `limited` is `true`.
411    pub fn is_empty(&self) -> bool {
412        !self.limited && self.prev_batch.is_none() && self.events.is_empty()
413    }
414}
415
416/// State changes in a room.
417#[derive(Clone, Debug, Serialize)]
418#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
419pub enum State {
420    /// The state changes between the previous sync and the **start** of the timeline.
421    ///
422    /// To get the full list of state changes since the previous sync, the state events in
423    /// [`Timeline`] must be added to these events to update the local state.
424    ///
425    /// With the `unstable-msc4222` feature, to get this variant, `use_state_after` must be set to
426    /// `false` in the [`Request`], which is the default.
427    #[serde(rename = "state")]
428    Before(StateEvents),
429
430    /// The state changes between the previous sync and the **end** of the timeline.
431    ///
432    /// This contains the full list of state changes since the previous sync. State events in
433    /// [`Timeline`] must be ignored to update the local state.
434    ///
435    /// To get this variant, `use_state_after` must be set to `true` in the [`Request`].
436    #[cfg(feature = "unstable-msc4222")]
437    #[serde(rename = "org.matrix.msc4222.state_after")]
438    After(StateEvents),
439}
440
441impl State {
442    /// Returns true if this is the `Before` variant and there are no state updates.
443    fn is_before_and_empty(&self) -> bool {
444        as_variant!(self, Self::Before).is_some_and(|state| state.is_empty())
445    }
446
447    /// Returns true if there are no state updates.
448    pub fn is_empty(&self) -> bool {
449        match self {
450            Self::Before(state) => state.is_empty(),
451            #[cfg(feature = "unstable-msc4222")]
452            Self::After(state) => state.is_empty(),
453        }
454    }
455}
456
457impl Default for State {
458    fn default() -> Self {
459        Self::Before(Default::default())
460    }
461}
462
463/// State events in the room.
464#[derive(Clone, Debug, Default, Deserialize, Serialize)]
465#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
466pub struct StateEvents {
467    /// A list of state events.
468    #[serde(default, skip_serializing_if = "Vec::is_empty")]
469    pub events: Vec<Raw<AnySyncStateEvent>>,
470}
471
472impl StateEvents {
473    /// Creates an empty `State`.
474    pub fn new() -> Self {
475        Default::default()
476    }
477
478    /// Returns true if there are no state updates.
479    pub fn is_empty(&self) -> bool {
480        self.events.is_empty()
481    }
482
483    /// Creates a `State` with events
484    pub fn with_events(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
485        Self { events, ..Default::default() }
486    }
487}
488
489impl From<Vec<Raw<AnySyncStateEvent>>> for StateEvents {
490    fn from(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
491        Self::with_events(events)
492    }
493}
494
495/// The global private data created by this user.
496#[derive(Clone, Debug, Default, Deserialize, Serialize)]
497#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
498pub struct GlobalAccountData {
499    /// A list of events.
500    #[serde(default, skip_serializing_if = "Vec::is_empty")]
501    pub events: Vec<Raw<AnyGlobalAccountDataEvent>>,
502}
503
504impl GlobalAccountData {
505    /// Creates an empty `GlobalAccountData`.
506    pub fn new() -> Self {
507        Default::default()
508    }
509
510    /// Returns true if there are no global account data updates.
511    pub fn is_empty(&self) -> bool {
512        self.events.is_empty()
513    }
514}
515
516/// The private data that this user has attached to this room.
517#[derive(Clone, Debug, Default, Deserialize, Serialize)]
518#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
519pub struct RoomAccountData {
520    /// A list of events.
521    #[serde(default, skip_serializing_if = "Vec::is_empty")]
522    pub events: Vec<Raw<AnyRoomAccountDataEvent>>,
523}
524
525impl RoomAccountData {
526    /// Creates an empty `RoomAccountData`.
527    pub fn new() -> Self {
528        Default::default()
529    }
530
531    /// Returns true if there are no room account data updates.
532    pub fn is_empty(&self) -> bool {
533        self.events.is_empty()
534    }
535}
536
537/// Ephemeral events not recorded in the timeline or state of the room.
538#[derive(Clone, Debug, Default, Deserialize, Serialize)]
539#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
540pub struct Ephemeral {
541    /// A list of events.
542    #[serde(default, skip_serializing_if = "Vec::is_empty")]
543    pub events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
544}
545
546impl Ephemeral {
547    /// Creates an empty `Ephemeral`.
548    pub fn new() -> Self {
549        Default::default()
550    }
551
552    /// Returns true if there are no ephemeral event updates.
553    pub fn is_empty(&self) -> bool {
554        self.events.is_empty()
555    }
556}
557
558/// Information about room for rendering to clients.
559#[derive(Clone, Debug, Default, Deserialize, Serialize)]
560#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
561pub struct RoomSummary {
562    /// Users which can be used to generate a room name if the room does not have one.
563    ///
564    /// Required if room name or canonical aliases are not set or empty.
565    #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")]
566    pub heroes: Vec<OwnedUserId>,
567
568    /// Number of users whose membership status is `join`.
569    /// Required if field has changed since last sync; otherwise, it may be
570    /// omitted.
571    #[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
572    pub joined_member_count: Option<UInt>,
573
574    /// Number of users whose membership status is `invite`.
575    /// Required if field has changed since last sync; otherwise, it may be
576    /// omitted.
577    #[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
578    pub invited_member_count: Option<UInt>,
579}
580
581impl RoomSummary {
582    /// Creates an empty `RoomSummary`.
583    pub fn new() -> Self {
584        Default::default()
585    }
586
587    /// Returns true if there are no room summary updates.
588    pub fn is_empty(&self) -> bool {
589        self.heroes.is_empty()
590            && self.joined_member_count.is_none()
591            && self.invited_member_count.is_none()
592    }
593}
594
595/// Updates to the rooms that the user has been invited to.
596#[derive(Clone, Debug, Default, Deserialize, Serialize)]
597#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
598pub struct InvitedRoom {
599    /// The state of a room that the user has been invited to.
600    #[serde(default, skip_serializing_if = "InviteState::is_empty")]
601    pub invite_state: InviteState,
602}
603
604impl InvitedRoom {
605    /// Creates an empty `InvitedRoom`.
606    pub fn new() -> Self {
607        Default::default()
608    }
609
610    /// Returns true if there are no updates to this room.
611    pub fn is_empty(&self) -> bool {
612        self.invite_state.is_empty()
613    }
614}
615
616impl From<InviteState> for InvitedRoom {
617    fn from(invite_state: InviteState) -> Self {
618        InvitedRoom { invite_state, ..Default::default() }
619    }
620}
621
622/// The state of a room that the user has been invited to.
623#[derive(Clone, Debug, Default, Deserialize, Serialize)]
624#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
625pub struct InviteState {
626    /// A list of state events.
627    #[serde(default, skip_serializing_if = "Vec::is_empty")]
628    pub events: Vec<Raw<AnyStrippedStateEvent>>,
629}
630
631impl InviteState {
632    /// Creates an empty `InviteState`.
633    pub fn new() -> Self {
634        Default::default()
635    }
636
637    /// Returns true if there are no state updates.
638    pub fn is_empty(&self) -> bool {
639        self.events.is_empty()
640    }
641}
642
643impl From<Vec<Raw<AnyStrippedStateEvent>>> for InviteState {
644    fn from(events: Vec<Raw<AnyStrippedStateEvent>>) -> Self {
645        InviteState { events, ..Default::default() }
646    }
647}
648
649/// Updates to the presence status of other users.
650#[derive(Clone, Debug, Default, Deserialize, Serialize)]
651#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
652pub struct Presence {
653    /// A list of events.
654    #[serde(default, skip_serializing_if = "Vec::is_empty")]
655    pub events: Vec<Raw<PresenceEvent>>,
656}
657
658impl Presence {
659    /// Creates an empty `Presence`.
660    pub fn new() -> Self {
661        Default::default()
662    }
663
664    /// Returns true if there are no presence updates.
665    pub fn is_empty(&self) -> bool {
666        self.events.is_empty()
667    }
668}
669
670/// Messages sent directly between devices.
671#[derive(Clone, Debug, Default, Deserialize, Serialize)]
672#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
673pub struct ToDevice {
674    /// A list of to-device events.
675    #[serde(default, skip_serializing_if = "Vec::is_empty")]
676    pub events: Vec<Raw<AnyToDeviceEvent>>,
677}
678
679impl ToDevice {
680    /// Creates an empty `ToDevice`.
681    pub fn new() -> Self {
682        Default::default()
683    }
684
685    /// Returns true if there are no to-device events.
686    pub fn is_empty(&self) -> bool {
687        self.events.is_empty()
688    }
689}
690
691#[cfg(test)]
692mod tests {
693    use assign::assign;
694    use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
695
696    use super::Timeline;
697
698    #[test]
699    fn timeline_serde() {
700        let timeline = assign!(Timeline::new(), { limited: true });
701        let timeline_serialized = json!({ "events": [], "limited": true });
702        assert_eq!(to_json_value(timeline).unwrap(), timeline_serialized);
703
704        let timeline_deserialized = from_json_value::<Timeline>(timeline_serialized).unwrap();
705        assert!(timeline_deserialized.limited);
706
707        let timeline_default = Timeline::default();
708        assert_eq!(to_json_value(timeline_default).unwrap(), json!({ "events": [] }));
709
710        let timeline_default_deserialized =
711            from_json_value::<Timeline>(json!({ "events": [] })).unwrap();
712        assert!(!timeline_default_deserialized.limited);
713    }
714}
715
716#[cfg(all(test, feature = "client"))]
717mod client_tests {
718    use std::time::Duration;
719
720    use assert_matches2::assert_matches;
721    use ruma_common::{
722        api::{
723            IncomingResponse as _, MatrixVersion, OutgoingRequest as _, SendAccessToken,
724            SupportedVersions,
725        },
726        event_id, room_id, user_id, RoomVersionId,
727    };
728    use ruma_events::AnyStrippedStateEvent;
729    use serde_json::{json, to_vec as to_json_vec, Value as JsonValue};
730
731    use super::{Filter, PresenceState, Request, Response, State};
732
733    fn sync_state_event() -> JsonValue {
734        json!({
735            "content": {
736              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
737              "displayname": "Alice Margatroid",
738              "membership": "join",
739            },
740            "event_id": "$143273582443PhrSn",
741            "origin_server_ts": 1_432_735_824,
742            "sender": "@alice:example.org",
743            "state_key": "@alice:example.org",
744            "type": "m.room.member",
745            "unsigned": {
746              "age": 1234,
747              "membership": "join",
748            },
749        })
750    }
751
752    #[test]
753    fn serialize_request_all_params() {
754        let supported = SupportedVersions {
755            versions: [MatrixVersion::V1_1].into(),
756            features: Default::default(),
757        };
758        let req: http::Request<Vec<u8>> = Request {
759            filter: Some(Filter::FilterId("66696p746572".to_owned())),
760            since: Some("s72594_4483_1934".to_owned()),
761            full_state: true,
762            set_presence: PresenceState::Offline,
763            timeout: Some(Duration::from_millis(30000)),
764            #[cfg(feature = "unstable-msc4222")]
765            use_state_after: true,
766        }
767        .try_into_http_request(
768            "https://homeserver.tld",
769            SendAccessToken::IfRequired("auth_tok"),
770            &supported,
771        )
772        .unwrap();
773
774        let uri = req.uri();
775        let query = uri.query().unwrap();
776
777        assert_eq!(uri.path(), "/_matrix/client/v3/sync");
778        assert!(query.contains("filter=66696p746572"));
779        assert!(query.contains("since=s72594_4483_1934"));
780        assert!(query.contains("full_state=true"));
781        assert!(query.contains("set_presence=offline"));
782        assert!(query.contains("timeout=30000"));
783        #[cfg(feature = "unstable-msc4222")]
784        assert!(query.contains("org.matrix.msc4222.use_state_after=true"));
785    }
786
787    #[test]
788    fn deserialize_response_invite() {
789        let creator = user_id!("@creator:localhost");
790        let invitee = user_id!("@invitee:localhost");
791        let room_id = room_id!("!privateroom:localhost");
792        let event_id = event_id!("$invite");
793
794        let body = json!({
795            "next_batch": "a00",
796            "rooms": {
797                "invite": {
798                    room_id: {
799                        "invite_state": {
800                            "events": [
801                                {
802                                    "content": {
803                                        "room_version": "11",
804                                    },
805                                    "type": "m.room.create",
806                                    "state_key": "",
807                                    "sender": creator,
808                                },
809                                {
810                                    "content": {
811                                        "membership": "invite",
812                                    },
813                                    "type": "m.room.member",
814                                    "state_key": invitee,
815                                    "sender": creator,
816                                    "origin_server_ts": 4_345_456,
817                                    "event_id": event_id,
818                                },
819                            ],
820                        },
821                    },
822                },
823            },
824        });
825        let http_response = http::Response::new(to_json_vec(&body).unwrap());
826
827        let response = Response::try_from_http_response(http_response).unwrap();
828        assert_eq!(response.next_batch, "a00");
829        let private_room = response.rooms.invite.get(room_id).unwrap();
830
831        let first_event = private_room.invite_state.events[0].deserialize().unwrap();
832        assert_matches!(first_event, AnyStrippedStateEvent::RoomCreate(create_event));
833        assert_eq!(create_event.sender, creator);
834        assert_eq!(create_event.content.room_version, RoomVersionId::V11);
835    }
836
837    #[test]
838    fn deserialize_response_no_state() {
839        let joined_room_id = room_id!("!joined:localhost");
840        let left_room_id = room_id!("!left:localhost");
841        let event = sync_state_event();
842
843        let body = json!({
844            "next_batch": "aaa",
845            "rooms": {
846                "join": {
847                    joined_room_id: {
848                        "timeline": {
849                            "events": [
850                                event,
851                            ],
852                        },
853                    },
854                },
855                "leave": {
856                    left_room_id: {
857                        "timeline": {
858                            "events": [
859                                event,
860                            ],
861                        },
862                    },
863                },
864            },
865        });
866
867        let http_response = http::Response::new(to_json_vec(&body).unwrap());
868
869        let response = Response::try_from_http_response(http_response).unwrap();
870        assert_eq!(response.next_batch, "aaa");
871
872        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
873        assert_eq!(joined_room.timeline.events.len(), 1);
874        assert!(joined_room.state.is_before_and_empty());
875
876        let left_room = response.rooms.leave.get(left_room_id).unwrap();
877        assert_eq!(left_room.timeline.events.len(), 1);
878        assert!(left_room.state.is_before_and_empty());
879    }
880
881    #[test]
882    fn deserialize_response_state_before() {
883        let joined_room_id = room_id!("!joined:localhost");
884        let left_room_id = room_id!("!left:localhost");
885        let event = sync_state_event();
886
887        let body = json!({
888            "next_batch": "aaa",
889            "rooms": {
890                "join": {
891                    joined_room_id: {
892                        "state": {
893                            "events": [
894                                event,
895                            ],
896                        },
897                    },
898                },
899                "leave": {
900                    left_room_id: {
901                        "state": {
902                            "events": [
903                                event,
904                            ],
905                        },
906                    },
907                },
908            },
909        });
910
911        let http_response = http::Response::new(to_json_vec(&body).unwrap());
912
913        let response = Response::try_from_http_response(http_response).unwrap();
914        assert_eq!(response.next_batch, "aaa");
915
916        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
917        assert!(joined_room.timeline.is_empty());
918        assert_matches!(&joined_room.state, State::Before(state));
919        assert_eq!(state.events.len(), 1);
920
921        let left_room = response.rooms.leave.get(left_room_id).unwrap();
922        assert!(left_room.timeline.is_empty());
923        assert_matches!(&left_room.state, State::Before(state));
924        assert_eq!(state.events.len(), 1);
925    }
926
927    #[test]
928    #[cfg(feature = "unstable-msc4222")]
929    fn deserialize_response_empty_state_after() {
930        let joined_room_id = room_id!("!joined:localhost");
931        let left_room_id = room_id!("!left:localhost");
932
933        let body = json!({
934            "next_batch": "aaa",
935            "rooms": {
936                "join": {
937                    joined_room_id: {
938                        "org.matrix.msc4222.state_after": {},
939                    },
940                },
941                "leave": {
942                    left_room_id: {
943                        "org.matrix.msc4222.state_after": {},
944                    },
945                },
946            },
947        });
948
949        let http_response = http::Response::new(to_json_vec(&body).unwrap());
950
951        let response = Response::try_from_http_response(http_response).unwrap();
952        assert_eq!(response.next_batch, "aaa");
953
954        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
955        assert!(joined_room.timeline.is_empty());
956        assert_matches!(&joined_room.state, State::After(state));
957        assert_eq!(state.events.len(), 0);
958
959        let left_room = response.rooms.leave.get(left_room_id).unwrap();
960        assert!(left_room.timeline.is_empty());
961        assert_matches!(&left_room.state, State::After(state));
962        assert_eq!(state.events.len(), 0);
963    }
964
965    #[test]
966    #[cfg(feature = "unstable-msc4222")]
967    fn deserialize_response_non_empty_state_after() {
968        let joined_room_id = room_id!("!joined:localhost");
969        let left_room_id = room_id!("!left:localhost");
970        let event = sync_state_event();
971
972        let body = json!({
973            "next_batch": "aaa",
974            "rooms": {
975                "join": {
976                    joined_room_id: {
977                        "org.matrix.msc4222.state_after": {
978                            "events": [
979                                event,
980                            ],
981                        },
982                    },
983                },
984                "leave": {
985                    left_room_id: {
986                        "org.matrix.msc4222.state_after": {
987                            "events": [
988                                event,
989                            ],
990                        },
991                    },
992                },
993            },
994        });
995
996        let http_response = http::Response::new(to_json_vec(&body).unwrap());
997
998        let response = Response::try_from_http_response(http_response).unwrap();
999        assert_eq!(response.next_batch, "aaa");
1000
1001        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1002        assert!(joined_room.timeline.is_empty());
1003        assert_matches!(&joined_room.state, State::After(state));
1004        assert_eq!(state.events.len(), 1);
1005
1006        let left_room = response.rooms.leave.get(left_room_id).unwrap();
1007        assert!(left_room.timeline.is_empty());
1008        assert_matches!(&left_room.state, State::After(state));
1009        assert_eq!(state.events.len(), 1);
1010    }
1011}
1012
1013#[cfg(all(test, feature = "server"))]
1014mod server_tests {
1015    use std::time::Duration;
1016
1017    use assert_matches2::assert_matches;
1018    use ruma_common::{
1019        api::{IncomingRequest as _, OutgoingResponse as _},
1020        owned_room_id,
1021        presence::PresenceState,
1022        serde::Raw,
1023    };
1024    use ruma_events::AnySyncStateEvent;
1025    use serde_json::{from_slice as from_json_slice, json, Value as JsonValue};
1026
1027    use super::{Filter, JoinedRoom, LeftRoom, Request, Response, State};
1028
1029    fn sync_state_event() -> Raw<AnySyncStateEvent> {
1030        Raw::new(&json!({
1031            "content": {
1032              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1033              "displayname": "Alice Margatroid",
1034              "membership": "join",
1035            },
1036            "event_id": "$143273582443PhrSn",
1037            "origin_server_ts": 1_432_735_824,
1038            "sender": "@alice:example.org",
1039            "state_key": "@alice:example.org",
1040            "type": "m.room.member",
1041            "unsigned": {
1042              "age": 1234,
1043              "membership": "join",
1044            },
1045        }))
1046        .unwrap()
1047        .cast_unchecked()
1048    }
1049
1050    #[test]
1051    fn deserialize_request_all_query_params() {
1052        let uri = http::Uri::builder()
1053            .scheme("https")
1054            .authority("matrix.org")
1055            .path_and_query(
1056                "/_matrix/client/r0/sync\
1057                ?filter=myfilter\
1058                &since=myts\
1059                &full_state=false\
1060                &set_presence=offline\
1061                &timeout=5000",
1062            )
1063            .build()
1064            .unwrap();
1065
1066        let req = Request::try_from_http_request(
1067            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1068            &[] as &[String],
1069        )
1070        .unwrap();
1071
1072        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1073        assert_eq!(id, "myfilter");
1074        assert_eq!(req.since.as_deref(), Some("myts"));
1075        assert!(!req.full_state);
1076        assert_eq!(req.set_presence, PresenceState::Offline);
1077        assert_eq!(req.timeout, Some(Duration::from_millis(5000)));
1078    }
1079
1080    #[test]
1081    fn deserialize_request_no_query_params() {
1082        let uri = http::Uri::builder()
1083            .scheme("https")
1084            .authority("matrix.org")
1085            .path_and_query("/_matrix/client/r0/sync")
1086            .build()
1087            .unwrap();
1088
1089        let req = Request::try_from_http_request(
1090            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1091            &[] as &[String],
1092        )
1093        .unwrap();
1094
1095        assert_matches!(req.filter, None);
1096        assert_eq!(req.since, None);
1097        assert!(!req.full_state);
1098        assert_eq!(req.set_presence, PresenceState::Online);
1099        assert_eq!(req.timeout, None);
1100    }
1101
1102    #[test]
1103    fn deserialize_request_some_query_params() {
1104        let uri = http::Uri::builder()
1105            .scheme("https")
1106            .authority("matrix.org")
1107            .path_and_query(
1108                "/_matrix/client/r0/sync\
1109                ?filter=EOKFFmdZYF\
1110                &timeout=0",
1111            )
1112            .build()
1113            .unwrap();
1114
1115        let req = Request::try_from_http_request(
1116            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1117            &[] as &[String],
1118        )
1119        .unwrap();
1120
1121        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1122        assert_eq!(id, "EOKFFmdZYF");
1123        assert_eq!(req.since, None);
1124        assert!(!req.full_state);
1125        assert_eq!(req.set_presence, PresenceState::Online);
1126        assert_eq!(req.timeout, Some(Duration::from_millis(0)));
1127    }
1128
1129    #[test]
1130    fn serialize_response_no_state() {
1131        let joined_room_id = owned_room_id!("!joined:localhost");
1132        let left_room_id = owned_room_id!("!left:localhost");
1133        let event = sync_state_event();
1134
1135        let mut response = Response::new("aaa".to_owned());
1136
1137        let mut joined_room = JoinedRoom::new();
1138        joined_room.timeline.events.push(event.clone().cast());
1139        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1140
1141        let mut left_room = LeftRoom::new();
1142        left_room.timeline.events.push(event.clone().cast());
1143        response.rooms.leave.insert(left_room_id.clone(), left_room);
1144
1145        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1146
1147        assert_eq!(
1148            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1149            json!({
1150                "next_batch": "aaa",
1151                "rooms": {
1152                    "join": {
1153                        joined_room_id: {
1154                            "timeline": {
1155                                "events": [
1156                                    event,
1157                                ],
1158                            },
1159                        },
1160                    },
1161                    "leave": {
1162                        left_room_id: {
1163                            "timeline": {
1164                                "events": [
1165                                    event,
1166                                ],
1167                            },
1168                        },
1169                    },
1170                },
1171            })
1172        );
1173    }
1174
1175    #[test]
1176    fn serialize_response_state_before() {
1177        let joined_room_id = owned_room_id!("!joined:localhost");
1178        let left_room_id = owned_room_id!("!left:localhost");
1179        let event = sync_state_event();
1180
1181        let mut response = Response::new("aaa".to_owned());
1182
1183        let mut joined_room = JoinedRoom::new();
1184        joined_room.state = State::Before(vec![event.clone()].into());
1185        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1186
1187        let mut left_room = LeftRoom::new();
1188        left_room.state = State::Before(vec![event.clone()].into());
1189        response.rooms.leave.insert(left_room_id.clone(), left_room);
1190
1191        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1192
1193        assert_eq!(
1194            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1195            json!({
1196                "next_batch": "aaa",
1197                "rooms": {
1198                    "join": {
1199                        joined_room_id: {
1200                            "state": {
1201                                "events": [
1202                                    event,
1203                                ],
1204                            },
1205                        },
1206                    },
1207                    "leave": {
1208                        left_room_id: {
1209                            "state": {
1210                                "events": [
1211                                    event,
1212                                ],
1213                            },
1214                        },
1215                    },
1216                },
1217            })
1218        );
1219    }
1220
1221    #[test]
1222    #[cfg(feature = "unstable-msc4222")]
1223    fn serialize_response_empty_state_after() {
1224        let joined_room_id = owned_room_id!("!joined:localhost");
1225        let left_room_id = owned_room_id!("!left:localhost");
1226
1227        let mut response = Response::new("aaa".to_owned());
1228
1229        let mut joined_room = JoinedRoom::new();
1230        joined_room.state = State::After(Default::default());
1231        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1232
1233        let mut left_room = LeftRoom::new();
1234        left_room.state = State::After(Default::default());
1235        response.rooms.leave.insert(left_room_id.clone(), left_room);
1236
1237        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1238
1239        assert_eq!(
1240            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1241            json!({
1242                "next_batch": "aaa",
1243                "rooms": {
1244                    "join": {
1245                        joined_room_id: {
1246                            "org.matrix.msc4222.state_after": {},
1247                        },
1248                    },
1249                    "leave": {
1250                        left_room_id: {
1251                            "org.matrix.msc4222.state_after": {},
1252                        },
1253                    },
1254                },
1255            })
1256        );
1257    }
1258
1259    #[test]
1260    #[cfg(feature = "unstable-msc4222")]
1261    fn serialize_response_non_empty_state_after() {
1262        let joined_room_id = owned_room_id!("!joined:localhost");
1263        let left_room_id = owned_room_id!("!left:localhost");
1264        let event = sync_state_event();
1265
1266        let mut response = Response::new("aaa".to_owned());
1267
1268        let mut joined_room = JoinedRoom::new();
1269        joined_room.state = State::After(vec![event.clone()].into());
1270        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1271
1272        let mut left_room = LeftRoom::new();
1273        left_room.state = State::After(vec![event.clone()].into());
1274        response.rooms.leave.insert(left_room_id.clone(), left_room);
1275
1276        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1277
1278        assert_eq!(
1279            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1280            json!({
1281                "next_batch": "aaa",
1282                "rooms": {
1283                    "join": {
1284                        joined_room_id: {
1285                            "org.matrix.msc4222.state_after": {
1286                                "events": [
1287                                    event,
1288                                ],
1289                            },
1290                        },
1291                    },
1292                    "leave": {
1293                        left_room_id: {
1294                            "org.matrix.msc4222.state_after": {
1295                                "events": [
1296                                    event,
1297                                ],
1298                            },
1299                        },
1300                    },
1301                },
1302            })
1303        );
1304    }
1305}