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