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