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            IncomingResponse 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, to_vec as to_json_vec};
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        let http_response = http::Response::new(to_json_vec(&body).unwrap());
937
938        let response = Response::try_from_http_response(http_response).unwrap();
939        assert_eq!(response.next_batch, "a00");
940        let private_room = response.rooms.invite.get(room_id).unwrap();
941
942        let first_event = private_room.invite_state.events[0].deserialize().unwrap();
943        assert_matches!(first_event, AnyStrippedStateEvent::RoomCreate(create_event));
944        assert_eq!(create_event.sender, creator);
945        assert_eq!(create_event.content.room_version, RoomVersionId::V11);
946    }
947
948    #[test]
949    fn deserialize_response_no_state() {
950        let joined_room_id = room_id!("!joined:localhost");
951        let left_room_id = room_id!("!left:localhost");
952        let event = sync_state_event();
953
954        let body = json!({
955            "next_batch": "aaa",
956            "rooms": {
957                "join": {
958                    joined_room_id: {
959                        "timeline": {
960                            "events": [
961                                event,
962                            ],
963                        },
964                    },
965                },
966                "leave": {
967                    left_room_id: {
968                        "timeline": {
969                            "events": [
970                                event,
971                            ],
972                        },
973                    },
974                },
975            },
976        });
977
978        let http_response = http::Response::new(to_json_vec(&body).unwrap());
979
980        let response = Response::try_from_http_response(http_response).unwrap();
981        assert_eq!(response.next_batch, "aaa");
982
983        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
984        assert_eq!(joined_room.timeline.events.len(), 1);
985        assert!(joined_room.state.is_before_and_empty());
986
987        let left_room = response.rooms.leave.get(left_room_id).unwrap();
988        assert_eq!(left_room.timeline.events.len(), 1);
989        assert!(left_room.state.is_before_and_empty());
990    }
991
992    #[test]
993    fn deserialize_response_state_before() {
994        let joined_room_id = room_id!("!joined:localhost");
995        let left_room_id = room_id!("!left:localhost");
996        let event = sync_state_event();
997
998        let body = json!({
999            "next_batch": "aaa",
1000            "rooms": {
1001                "join": {
1002                    joined_room_id: {
1003                        "state": {
1004                            "events": [
1005                                event,
1006                            ],
1007                        },
1008                    },
1009                },
1010                "leave": {
1011                    left_room_id: {
1012                        "state": {
1013                            "events": [
1014                                event,
1015                            ],
1016                        },
1017                    },
1018                },
1019            },
1020        });
1021
1022        let http_response = http::Response::new(to_json_vec(&body).unwrap());
1023
1024        let response = Response::try_from_http_response(http_response).unwrap();
1025        assert_eq!(response.next_batch, "aaa");
1026
1027        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1028        assert!(joined_room.timeline.is_empty());
1029        assert_matches!(&joined_room.state, State::Before(state));
1030        assert_eq!(state.events.len(), 1);
1031
1032        let left_room = response.rooms.leave.get(left_room_id).unwrap();
1033        assert!(left_room.timeline.is_empty());
1034        assert_matches!(&left_room.state, State::Before(state));
1035        assert_eq!(state.events.len(), 1);
1036    }
1037
1038    #[test]
1039    fn deserialize_response_empty_state_after() {
1040        let joined_room_id = room_id!("!joined:localhost");
1041        let left_room_id = room_id!("!left:localhost");
1042
1043        let body = json!({
1044            "next_batch": "aaa",
1045            "rooms": {
1046                "join": {
1047                    joined_room_id: {
1048                        "state_after": {},
1049                    },
1050                },
1051                "leave": {
1052                    left_room_id: {
1053                        "state_after": {},
1054                    },
1055                },
1056            },
1057        });
1058
1059        let http_response = http::Response::new(to_json_vec(&body).unwrap());
1060
1061        let response = Response::try_from_http_response(http_response).unwrap();
1062        assert_eq!(response.next_batch, "aaa");
1063
1064        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1065        assert!(joined_room.timeline.is_empty());
1066        assert_matches!(&joined_room.state, State::After(state));
1067        assert_eq!(state.events.len(), 0);
1068
1069        let left_room = response.rooms.leave.get(left_room_id).unwrap();
1070        assert!(left_room.timeline.is_empty());
1071        assert_matches!(&left_room.state, State::After(state));
1072        assert_eq!(state.events.len(), 0);
1073    }
1074
1075    #[test]
1076    fn deserialize_response_non_empty_state_after() {
1077        let joined_room_id = room_id!("!joined:localhost");
1078        let left_room_id = room_id!("!left:localhost");
1079        let event = sync_state_event();
1080
1081        let body = json!({
1082            "next_batch": "aaa",
1083            "rooms": {
1084                "join": {
1085                    joined_room_id: {
1086                        "state_after": {
1087                            "events": [
1088                                event,
1089                            ],
1090                        },
1091                    },
1092                },
1093                "leave": {
1094                    left_room_id: {
1095                        "state_after": {
1096                            "events": [
1097                                event,
1098                            ],
1099                        },
1100                    },
1101                },
1102            },
1103        });
1104
1105        let http_response = http::Response::new(to_json_vec(&body).unwrap());
1106
1107        let response = Response::try_from_http_response(http_response).unwrap();
1108        assert_eq!(response.next_batch, "aaa");
1109
1110        let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1111        assert!(joined_room.timeline.is_empty());
1112        assert_matches!(&joined_room.state, State::After(state));
1113        assert_eq!(state.events.len(), 1);
1114
1115        let left_room = response.rooms.leave.get(left_room_id).unwrap();
1116        assert!(left_room.timeline.is_empty());
1117        assert_matches!(&left_room.state, State::After(state));
1118        assert_eq!(state.events.len(), 1);
1119    }
1120}
1121
1122#[cfg(all(test, feature = "server"))]
1123mod server_tests {
1124    use std::time::Duration;
1125
1126    use assert_matches2::assert_matches;
1127    use ruma_common::{
1128        api::{IncomingRequest as _, OutgoingResponse as _},
1129        owned_room_id,
1130        presence::PresenceState,
1131        serde::Raw,
1132    };
1133    use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent};
1134    use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
1135
1136    use super::{Filter, JoinedRoom, KnockedRoom, LeftRoom, Request, Response, State};
1137
1138    fn sync_state_event() -> Raw<AnySyncStateEvent> {
1139        Raw::new(&json!({
1140            "content": {
1141              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1142              "displayname": "Alice Margatroid",
1143              "membership": "join",
1144            },
1145            "event_id": "$143273582443PhrSn",
1146            "origin_server_ts": 1_432_735_824,
1147            "sender": "@alice:example.org",
1148            "state_key": "@alice:example.org",
1149            "type": "m.room.member",
1150            "unsigned": {
1151              "age": 1234,
1152              "membership": "join",
1153            },
1154        }))
1155        .unwrap()
1156        .cast_unchecked()
1157    }
1158
1159    #[test]
1160    fn deserialize_request_all_query_params() {
1161        let uri = http::Uri::builder()
1162            .scheme("https")
1163            .authority("matrix.org")
1164            .path_and_query(
1165                "/_matrix/client/r0/sync\
1166                ?filter=myfilter\
1167                &since=myts\
1168                &full_state=false\
1169                &set_presence=offline\
1170                &timeout=5000",
1171            )
1172            .build()
1173            .unwrap();
1174
1175        let req = Request::try_from_http_request(
1176            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1177            &[] as &[String],
1178        )
1179        .unwrap();
1180
1181        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1182        assert_eq!(id, "myfilter");
1183        assert_eq!(req.since.as_deref(), Some("myts"));
1184        assert!(!req.full_state);
1185        assert_eq!(req.set_presence, PresenceState::Offline);
1186        assert_eq!(req.timeout, Some(Duration::from_millis(5000)));
1187    }
1188
1189    #[test]
1190    fn deserialize_request_no_query_params() {
1191        let uri = http::Uri::builder()
1192            .scheme("https")
1193            .authority("matrix.org")
1194            .path_and_query("/_matrix/client/r0/sync")
1195            .build()
1196            .unwrap();
1197
1198        let req = Request::try_from_http_request(
1199            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1200            &[] as &[String],
1201        )
1202        .unwrap();
1203
1204        assert_matches!(req.filter, None);
1205        assert_eq!(req.since, None);
1206        assert!(!req.full_state);
1207        assert_eq!(req.set_presence, PresenceState::Online);
1208        assert_eq!(req.timeout, None);
1209    }
1210
1211    #[test]
1212    fn deserialize_request_some_query_params() {
1213        let uri = http::Uri::builder()
1214            .scheme("https")
1215            .authority("matrix.org")
1216            .path_and_query(
1217                "/_matrix/client/r0/sync\
1218                ?filter=EOKFFmdZYF\
1219                &timeout=0",
1220            )
1221            .build()
1222            .unwrap();
1223
1224        let req = Request::try_from_http_request(
1225            http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1226            &[] as &[String],
1227        )
1228        .unwrap();
1229
1230        assert_matches!(req.filter, Some(Filter::FilterId(id)));
1231        assert_eq!(id, "EOKFFmdZYF");
1232        assert_eq!(req.since, None);
1233        assert!(!req.full_state);
1234        assert_eq!(req.set_presence, PresenceState::Online);
1235        assert_eq!(req.timeout, Some(Duration::from_millis(0)));
1236    }
1237
1238    #[test]
1239    fn serialize_response_no_state() {
1240        let joined_room_id = owned_room_id!("!joined:localhost");
1241        let left_room_id = owned_room_id!("!left:localhost");
1242        let event = sync_state_event();
1243
1244        let mut response = Response::new("aaa".to_owned());
1245
1246        let mut joined_room = JoinedRoom::new();
1247        joined_room.timeline.events.push(event.clone().cast());
1248        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1249
1250        let mut left_room = LeftRoom::new();
1251        left_room.timeline.events.push(event.clone().cast());
1252        response.rooms.leave.insert(left_room_id.clone(), left_room);
1253
1254        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1255
1256        assert_eq!(
1257            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1258            json!({
1259                "next_batch": "aaa",
1260                "rooms": {
1261                    "join": {
1262                        joined_room_id: {
1263                            "timeline": {
1264                                "events": [
1265                                    event,
1266                                ],
1267                            },
1268                        },
1269                    },
1270                    "leave": {
1271                        left_room_id: {
1272                            "timeline": {
1273                                "events": [
1274                                    event,
1275                                ],
1276                            },
1277                        },
1278                    },
1279                },
1280            })
1281        );
1282    }
1283
1284    #[test]
1285    fn serialize_response_state_before() {
1286        let joined_room_id = owned_room_id!("!joined:localhost");
1287        let left_room_id = owned_room_id!("!left:localhost");
1288        let event = sync_state_event();
1289
1290        let mut response = Response::new("aaa".to_owned());
1291
1292        let mut joined_room = JoinedRoom::new();
1293        joined_room.state = State::Before(vec![event.clone()].into());
1294        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1295
1296        let mut left_room = LeftRoom::new();
1297        left_room.state = State::Before(vec![event.clone()].into());
1298        response.rooms.leave.insert(left_room_id.clone(), left_room);
1299
1300        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1301
1302        assert_eq!(
1303            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1304            json!({
1305                "next_batch": "aaa",
1306                "rooms": {
1307                    "join": {
1308                        joined_room_id: {
1309                            "state": {
1310                                "events": [
1311                                    event,
1312                                ],
1313                            },
1314                        },
1315                    },
1316                    "leave": {
1317                        left_room_id: {
1318                            "state": {
1319                                "events": [
1320                                    event,
1321                                ],
1322                            },
1323                        },
1324                    },
1325                },
1326            })
1327        );
1328    }
1329
1330    #[test]
1331    fn serialize_response_empty_state_after() {
1332        let joined_room_id = owned_room_id!("!joined:localhost");
1333        let left_room_id = owned_room_id!("!left:localhost");
1334
1335        let mut response = Response::new("aaa".to_owned());
1336
1337        let mut joined_room = JoinedRoom::new();
1338        joined_room.state = State::After(Default::default());
1339        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1340
1341        let mut left_room = LeftRoom::new();
1342        left_room.state = State::After(Default::default());
1343        response.rooms.leave.insert(left_room_id.clone(), left_room);
1344
1345        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1346
1347        assert_eq!(
1348            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1349            json!({
1350                "next_batch": "aaa",
1351                "rooms": {
1352                    "join": {
1353                        joined_room_id: {
1354                            "state_after": {},
1355                        },
1356                    },
1357                    "leave": {
1358                        left_room_id: {
1359                            "state_after": {},
1360                        },
1361                    },
1362                },
1363            })
1364        );
1365    }
1366
1367    #[test]
1368    fn serialize_response_non_empty_state_after() {
1369        let joined_room_id = owned_room_id!("!joined:localhost");
1370        let left_room_id = owned_room_id!("!left:localhost");
1371        let event = sync_state_event();
1372
1373        let mut response = Response::new("aaa".to_owned());
1374
1375        let mut joined_room = JoinedRoom::new();
1376        joined_room.state = State::After(vec![event.clone()].into());
1377        response.rooms.join.insert(joined_room_id.clone(), joined_room);
1378
1379        let mut left_room = LeftRoom::new();
1380        left_room.state = State::After(vec![event.clone()].into());
1381        response.rooms.leave.insert(left_room_id.clone(), left_room);
1382
1383        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1384
1385        assert_eq!(
1386            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1387            json!({
1388                "next_batch": "aaa",
1389                "rooms": {
1390                    "join": {
1391                        joined_room_id: {
1392                            "state_after": {
1393                                "events": [
1394                                    event,
1395                                ],
1396                            },
1397                        },
1398                    },
1399                    "leave": {
1400                        left_room_id: {
1401                            "state_after": {
1402                                "events": [
1403                                    event,
1404                                ],
1405                            },
1406                        },
1407                    },
1408                },
1409            })
1410        );
1411    }
1412
1413    #[test]
1414    fn serialize_response_knocked_room() {
1415        let knocked_room_id = owned_room_id!("!knocked:localhost");
1416        let event: Raw<AnyStrippedStateEvent> = Raw::new(&json!({
1417            "content": {
1418              "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1419              "displayname": "Alice Margatroid",
1420              "membership": "join",
1421            },
1422            "sender": "@alice:example.org",
1423            "state_key": "@alice:example.org",
1424            "type": "m.room.member",
1425        }))
1426        .unwrap()
1427        .cast_unchecked();
1428
1429        let mut response = Response::new("aaa".to_owned());
1430
1431        let mut knocked_room = KnockedRoom::new();
1432        knocked_room.knock_state.events.push(event.clone());
1433        response.rooms.knock.insert(knocked_room_id.clone(), knocked_room);
1434
1435        let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1436
1437        assert_eq!(
1438            from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1439            json!({
1440                "next_batch": "aaa",
1441                "rooms": {
1442                    "knock": {
1443                        knocked_room_id: {
1444                            "knock_state": {
1445                                "events": [
1446                                    event,
1447                                ],
1448                            },
1449                        },
1450                    },
1451                },
1452            })
1453        );
1454    }
1455}