Skip to main content

ruma_client_api/sync/sync_events/
v3.rs

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