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    OneTimeKeyAlgorithm, OwnedEventId, OwnedRoomId, OwnedUserId,
11    api::{auth_scheme::AccessToken, request, response},
12    metadata,
13    presence::PresenceState,
14    serde::Raw,
15};
16use ruma_events::{
17    AnyGlobalAccountDataEvent, AnyRoomAccountDataEvent, AnyStrippedStateEvent,
18    AnySyncEphemeralRoomEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyToDeviceEvent,
19    presence::PresenceEvent,
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 ruma_common::canonical_json::assert_to_canonical_json_eq;
688    use serde_json::{from_value as from_json_value, json};
689
690    use super::Timeline;
691
692    #[test]
693    fn timeline_serde() {
694        let timeline = assign!(Timeline::new(), { limited: true });
695        let timeline_serialized = json!({ "events": [], "limited": true });
696        assert_to_canonical_json_eq!(timeline, timeline_serialized.clone());
697
698        let timeline_deserialized = from_json_value::<Timeline>(timeline_serialized).unwrap();
699        assert!(timeline_deserialized.limited);
700
701        let timeline_default = Timeline::default();
702        assert_to_canonical_json_eq!(timeline_default, json!({ "events": [] }));
703
704        let timeline_default_deserialized =
705            from_json_value::<Timeline>(json!({ "events": [] })).unwrap();
706        assert!(!timeline_default_deserialized.limited);
707    }
708}
709
710#[cfg(all(test, feature = "client"))]
711mod client_tests {
712    use std::{borrow::Cow, time::Duration};
713
714    use assert_matches2::assert_matches;
715    use ruma_common::{
716        RoomVersionId,
717        api::{
718            IncomingResponse as _, MatrixVersion, OutgoingRequest as _, SupportedVersions,
719            auth_scheme::SendAccessToken,
720        },
721        event_id, room_id, user_id,
722    };
723    use ruma_events::AnyStrippedStateEvent;
724    use serde_json::{Value as JsonValue, json, to_vec as to_json_vec};
725
726    use super::{Filter, PresenceState, Request, Response, State};
727
728    fn sync_state_event() -> JsonValue {
729        json!({
730            "content": {
731              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
732              "displayname": "Alice Margatroid",
733              "membership": "join",
734            },
735            "event_id": "$143273582443PhrSn",
736            "origin_server_ts": 1_432_735_824,
737            "sender": "@alice:example.org",
738            "state_key": "@alice:example.org",
739            "type": "m.room.member",
740            "unsigned": {
741              "age": 1234,
742              "membership": "join",
743            },
744        })
745    }
746
747    #[test]
748    fn serialize_request_all_params() {
749        let supported = SupportedVersions {
750            versions: [MatrixVersion::V1_1].into(),
751            features: Default::default(),
752        };
753        let req: http::Request<Vec<u8>> = Request {
754            filter: Some(Filter::FilterId("66696p746572".to_owned())),
755            since: Some("s72594_4483_1934".to_owned()),
756            full_state: true,
757            set_presence: PresenceState::Offline,
758            timeout: Some(Duration::from_millis(30000)),
759            use_state_after: true,
760        }
761        .try_into_http_request(
762            "https://homeserver.tld",
763            SendAccessToken::IfRequired("auth_tok"),
764            Cow::Owned(supported),
765        )
766        .unwrap();
767
768        let uri = req.uri();
769        let query = uri.query().unwrap();
770
771        assert_eq!(uri.path(), "/_matrix/client/v3/sync");
772        assert!(query.contains("filter=66696p746572"));
773        assert!(query.contains("since=s72594_4483_1934"));
774        assert!(query.contains("full_state=true"));
775        assert!(query.contains("set_presence=offline"));
776        assert!(query.contains("timeout=30000"));
777        assert!(query.contains("use_state_after=true"));
778    }
779
780    #[test]
781    fn deserialize_response_invite() {
782        let creator = user_id!("@creator:localhost");
783        let invitee = user_id!("@invitee:localhost");
784        let room_id = room_id!("!privateroom:localhost");
785        let event_id = event_id!("$invite");
786
787        let body = json!({
788            "next_batch": "a00",
789            "rooms": {
790                "invite": {
791                    room_id: {
792                        "invite_state": {
793                            "events": [
794                                {
795                                    "content": {
796                                        "room_version": "11",
797                                    },
798                                    "type": "m.room.create",
799                                    "state_key": "",
800                                    "sender": creator,
801                                },
802                                {
803                                    "content": {
804                                        "membership": "invite",
805                                    },
806                                    "type": "m.room.member",
807                                    "state_key": invitee,
808                                    "sender": creator,
809                                    "origin_server_ts": 4_345_456,
810                                    "event_id": event_id,
811                                },
812                            ],
813                        },
814                    },
815                },
816            },
817        });
818        let http_response = http::Response::new(to_json_vec(&body).unwrap());
819
820        let response = Response::try_from_http_response(http_response).unwrap();
821        assert_eq!(response.next_batch, "a00");
822        let private_room = response.rooms.invite.get(room_id).unwrap();
823
824        let first_event = private_room.invite_state.events[0].deserialize().unwrap();
825        assert_matches!(first_event, AnyStrippedStateEvent::RoomCreate(create_event));
826        assert_eq!(create_event.sender, creator);
827        assert_eq!(create_event.content.room_version, RoomVersionId::V11);
828    }
829
830    #[test]
831    fn deserialize_response_no_state() {
832        let joined_room_id = room_id!("!joined:localhost");
833        let left_room_id = room_id!("!left:localhost");
834        let event = sync_state_event();
835
836        let body = json!({
837            "next_batch": "aaa",
838            "rooms": {
839                "join": {
840                    joined_room_id: {
841                        "timeline": {
842                            "events": [
843                                event,
844                            ],
845                        },
846                    },
847                },
848                "leave": {
849                    left_room_id: {
850                        "timeline": {
851                            "events": [
852                                event,
853                            ],
854                        },
855                    },
856                },
857            },
858        });
859
860        let http_response = http::Response::new(to_json_vec(&body).unwrap());
861
862        let response = Response::try_from_http_response(http_response).unwrap();
863        assert_eq!(response.next_batch, "aaa");
864
865        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
866        assert_eq!(joined_room.timeline.events.len(), 1);
867        assert!(joined_room.state.is_before_and_empty());
868
869        let left_room = response.rooms.leave.get(left_room_id).unwrap();
870        assert_eq!(left_room.timeline.events.len(), 1);
871        assert!(left_room.state.is_before_and_empty());
872    }
873
874    #[test]
875    fn deserialize_response_state_before() {
876        let joined_room_id = room_id!("!joined:localhost");
877        let left_room_id = room_id!("!left:localhost");
878        let event = sync_state_event();
879
880        let body = json!({
881            "next_batch": "aaa",
882            "rooms": {
883                "join": {
884                    joined_room_id: {
885                        "state": {
886                            "events": [
887                                event,
888                            ],
889                        },
890                    },
891                },
892                "leave": {
893                    left_room_id: {
894                        "state": {
895                            "events": [
896                                event,
897                            ],
898                        },
899                    },
900                },
901            },
902        });
903
904        let http_response = http::Response::new(to_json_vec(&body).unwrap());
905
906        let response = Response::try_from_http_response(http_response).unwrap();
907        assert_eq!(response.next_batch, "aaa");
908
909        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
910        assert!(joined_room.timeline.is_empty());
911        assert_matches!(&joined_room.state, State::Before(state));
912        assert_eq!(state.events.len(), 1);
913
914        let left_room = response.rooms.leave.get(left_room_id).unwrap();
915        assert!(left_room.timeline.is_empty());
916        assert_matches!(&left_room.state, State::Before(state));
917        assert_eq!(state.events.len(), 1);
918    }
919
920    #[test]
921    fn deserialize_response_empty_state_after() {
922        let joined_room_id = room_id!("!joined:localhost");
923        let left_room_id = room_id!("!left:localhost");
924
925        let body = json!({
926            "next_batch": "aaa",
927            "rooms": {
928                "join": {
929                    joined_room_id: {
930                        "state_after": {},
931                    },
932                },
933                "leave": {
934                    left_room_id: {
935                        "state_after": {},
936                    },
937                },
938            },
939        });
940
941        let http_response = http::Response::new(to_json_vec(&body).unwrap());
942
943        let response = Response::try_from_http_response(http_response).unwrap();
944        assert_eq!(response.next_batch, "aaa");
945
946        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
947        assert!(joined_room.timeline.is_empty());
948        assert_matches!(&joined_room.state, State::After(state));
949        assert_eq!(state.events.len(), 0);
950
951        let left_room = response.rooms.leave.get(left_room_id).unwrap();
952        assert!(left_room.timeline.is_empty());
953        assert_matches!(&left_room.state, State::After(state));
954        assert_eq!(state.events.len(), 0);
955    }
956
957    #[test]
958    fn deserialize_response_non_empty_state_after() {
959        let joined_room_id = room_id!("!joined:localhost");
960        let left_room_id = room_id!("!left:localhost");
961        let event = sync_state_event();
962
963        let body = json!({
964            "next_batch": "aaa",
965            "rooms": {
966                "join": {
967                    joined_room_id: {
968                        "state_after": {
969                            "events": [
970                                event,
971                            ],
972                        },
973                    },
974                },
975                "leave": {
976                    left_room_id: {
977                        "state_after": {
978                            "events": [
979                                event,
980                            ],
981                        },
982                    },
983                },
984            },
985        });
986
987        let http_response = http::Response::new(to_json_vec(&body).unwrap());
988
989        let response = Response::try_from_http_response(http_response).unwrap();
990        assert_eq!(response.next_batch, "aaa");
991
992        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
993        assert!(joined_room.timeline.is_empty());
994        assert_matches!(&joined_room.state, State::After(state));
995        assert_eq!(state.events.len(), 1);
996
997        let left_room = response.rooms.leave.get(left_room_id).unwrap();
998        assert!(left_room.timeline.is_empty());
999        assert_matches!(&left_room.state, State::After(state));
1000        assert_eq!(state.events.len(), 1);
1001    }
1002}
1003
1004#[cfg(all(test, feature = "server"))]
1005mod server_tests {
1006    use std::time::Duration;
1007
1008    use assert_matches2::assert_matches;
1009    use ruma_common::{
1010        api::{IncomingRequest as _, OutgoingResponse as _},
1011        owned_room_id,
1012        presence::PresenceState,
1013        serde::Raw,
1014    };
1015    use ruma_events::AnySyncStateEvent;
1016    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
1017
1018    use super::{Filter, JoinedRoom, LeftRoom, Request, Response, State};
1019
1020    fn sync_state_event() -> Raw<AnySyncStateEvent> {
1021        Raw::new(&json!({
1022            "content": {
1023              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1024              "displayname": "Alice Margatroid",
1025              "membership": "join",
1026            },
1027            "event_id": "$143273582443PhrSn",
1028            "origin_server_ts": 1_432_735_824,
1029            "sender": "@alice:example.org",
1030            "state_key": "@alice:example.org",
1031            "type": "m.room.member",
1032            "unsigned": {
1033              "age": 1234,
1034              "membership": "join",
1035            },
1036        }))
1037        .unwrap()
1038        .cast_unchecked()
1039    }
1040
1041    #[test]
1042    fn deserialize_request_all_query_params() {
1043        let uri = http::Uri::builder()
1044            .scheme("https")
1045            .authority("matrix.org")
1046            .path_and_query(
1047                "/_matrix/client/r0/sync\
1048                ?filter=myfilter\
1049                &since=myts\
1050                &full_state=false\
1051                &set_presence=offline\
1052                &timeout=5000",
1053            )
1054            .build()
1055            .unwrap();
1056
1057        let req = Request::try_from_http_request(
1058            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1059            &[] as &[String],
1060        )
1061        .unwrap();
1062
1063        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1064        assert_eq!(id, "myfilter");
1065        assert_eq!(req.since.as_deref(), Some("myts"));
1066        assert!(!req.full_state);
1067        assert_eq!(req.set_presence, PresenceState::Offline);
1068        assert_eq!(req.timeout, Some(Duration::from_millis(5000)));
1069    }
1070
1071    #[test]
1072    fn deserialize_request_no_query_params() {
1073        let uri = http::Uri::builder()
1074            .scheme("https")
1075            .authority("matrix.org")
1076            .path_and_query("/_matrix/client/r0/sync")
1077            .build()
1078            .unwrap();
1079
1080        let req = Request::try_from_http_request(
1081            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1082            &[] as &[String],
1083        )
1084        .unwrap();
1085
1086        assert_matches!(req.filter, None);
1087        assert_eq!(req.since, None);
1088        assert!(!req.full_state);
1089        assert_eq!(req.set_presence, PresenceState::Online);
1090        assert_eq!(req.timeout, None);
1091    }
1092
1093    #[test]
1094    fn deserialize_request_some_query_params() {
1095        let uri = http::Uri::builder()
1096            .scheme("https")
1097            .authority("matrix.org")
1098            .path_and_query(
1099                "/_matrix/client/r0/sync\
1100                ?filter=EOKFFmdZYF\
1101                &timeout=0",
1102            )
1103            .build()
1104            .unwrap();
1105
1106        let req = Request::try_from_http_request(
1107            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1108            &[] as &[String],
1109        )
1110        .unwrap();
1111
1112        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1113        assert_eq!(id, "EOKFFmdZYF");
1114        assert_eq!(req.since, None);
1115        assert!(!req.full_state);
1116        assert_eq!(req.set_presence, PresenceState::Online);
1117        assert_eq!(req.timeout, Some(Duration::from_millis(0)));
1118    }
1119
1120    #[test]
1121    fn serialize_response_no_state() {
1122        let joined_room_id = owned_room_id!("!joined:localhost");
1123        let left_room_id = owned_room_id!("!left:localhost");
1124        let event = sync_state_event();
1125
1126        let mut response = Response::new("aaa".to_owned());
1127
1128        let mut joined_room = JoinedRoom::new();
1129        joined_room.timeline.events.push(event.clone().cast());
1130        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1131
1132        let mut left_room = LeftRoom::new();
1133        left_room.timeline.events.push(event.clone().cast());
1134        response.rooms.leave.insert(left_room_id.clone(), left_room);
1135
1136        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1137
1138        assert_eq!(
1139            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1140            json!({
1141                "next_batch": "aaa",
1142                "rooms": {
1143                    "join": {
1144                        joined_room_id: {
1145                            "timeline": {
1146                                "events": [
1147                                    event,
1148                                ],
1149                            },
1150                        },
1151                    },
1152                    "leave": {
1153                        left_room_id: {
1154                            "timeline": {
1155                                "events": [
1156                                    event,
1157                                ],
1158                            },
1159                        },
1160                    },
1161                },
1162            })
1163        );
1164    }
1165
1166    #[test]
1167    fn serialize_response_state_before() {
1168        let joined_room_id = owned_room_id!("!joined:localhost");
1169        let left_room_id = owned_room_id!("!left:localhost");
1170        let event = sync_state_event();
1171
1172        let mut response = Response::new("aaa".to_owned());
1173
1174        let mut joined_room = JoinedRoom::new();
1175        joined_room.state = State::Before(vec![event.clone()].into());
1176        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1177
1178        let mut left_room = LeftRoom::new();
1179        left_room.state = State::Before(vec![event.clone()].into());
1180        response.rooms.leave.insert(left_room_id.clone(), left_room);
1181
1182        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1183
1184        assert_eq!(
1185            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1186            json!({
1187                "next_batch": "aaa",
1188                "rooms": {
1189                    "join": {
1190                        joined_room_id: {
1191                            "state": {
1192                                "events": [
1193                                    event,
1194                                ],
1195                            },
1196                        },
1197                    },
1198                    "leave": {
1199                        left_room_id: {
1200                            "state": {
1201                                "events": [
1202                                    event,
1203                                ],
1204                            },
1205                        },
1206                    },
1207                },
1208            })
1209        );
1210    }
1211
1212    #[test]
1213    fn serialize_response_empty_state_after() {
1214        let joined_room_id = owned_room_id!("!joined:localhost");
1215        let left_room_id = owned_room_id!("!left:localhost");
1216
1217        let mut response = Response::new("aaa".to_owned());
1218
1219        let mut joined_room = JoinedRoom::new();
1220        joined_room.state = State::After(Default::default());
1221        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1222
1223        let mut left_room = LeftRoom::new();
1224        left_room.state = State::After(Default::default());
1225        response.rooms.leave.insert(left_room_id.clone(), left_room);
1226
1227        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1228
1229        assert_eq!(
1230            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1231            json!({
1232                "next_batch": "aaa",
1233                "rooms": {
1234                    "join": {
1235                        joined_room_id: {
1236                            "state_after": {},
1237                        },
1238                    },
1239                    "leave": {
1240                        left_room_id: {
1241                            "state_after": {},
1242                        },
1243                    },
1244                },
1245            })
1246        );
1247    }
1248
1249    #[test]
1250    fn serialize_response_non_empty_state_after() {
1251        let joined_room_id = owned_room_id!("!joined:localhost");
1252        let left_room_id = owned_room_id!("!left:localhost");
1253        let event = sync_state_event();
1254
1255        let mut response = Response::new("aaa".to_owned());
1256
1257        let mut joined_room = JoinedRoom::new();
1258        joined_room.state = State::After(vec![event.clone()].into());
1259        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1260
1261        let mut left_room = LeftRoom::new();
1262        left_room.state = State::After(vec![event.clone()].into());
1263        response.rooms.leave.insert(left_room_id.clone(), left_room);
1264
1265        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1266
1267        assert_eq!(
1268            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1269            json!({
1270                "next_batch": "aaa",
1271                "rooms": {
1272                    "join": {
1273                        joined_room_id: {
1274                            "state_after": {
1275                                "events": [
1276                                    event,
1277                                ],
1278                            },
1279                        },
1280                    },
1281                    "leave": {
1282                        left_room_id: {
1283                            "state_after": {
1284                                "events": [
1285                                    event,
1286                                ],
1287                            },
1288                        },
1289                    },
1290                },
1291            })
1292        );
1293    }
1294}