1use 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]
40#[derive(Default)]
41pub struct Request {
42 #[serde(skip_serializing_if = "Option::is_none")]
44 #[ruma_api(query)]
45 pub filter: Option<Filter>,
46
47 #[serde(skip_serializing_if = "Option::is_none")]
52 #[ruma_api(query)]
53 pub since: Option<String>,
54
55 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
57 #[ruma_api(query)]
58 pub full_state: bool,
59
60 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
64 #[ruma_api(query)]
65 pub set_presence: PresenceState,
66
67 #[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 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
79 #[ruma_api(query)]
80 pub use_state_after: bool,
81}
82
83#[response]
85pub struct Response {
86 pub next_batch: String,
88
89 #[serde(default, skip_serializing_if = "Rooms::is_empty")]
91 pub rooms: Rooms,
92
93 #[serde(default, skip_serializing_if = "Presence::is_empty")]
95 pub presence: Presence,
96
97 #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
99 pub account_data: GlobalAccountData,
100
101 #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
103 pub to_device: ToDevice,
104
105 #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
109 pub device_lists: DeviceLists,
110
111 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
114 pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
121 pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
122}
123
124impl Request {
125 pub fn new() -> Self {
127 Default::default()
128 }
129}
130
131impl Response {
132 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#[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 #[serde(with = "ruma_common::serde::json_string")]
165 FilterDefinition(FilterDefinition),
166
167 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
185#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
186pub struct Rooms {
187 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
189 pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
190
191 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
193 pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
194
195 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
197 pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
198
199 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
201 pub knock: BTreeMap<OwnedRoomId, KnockedRoom>,
202}
203
204impl Rooms {
205 pub fn new() -> Self {
207 Default::default()
208 }
209
210 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#[derive(Clone, Debug, Default, Serialize)]
219#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
220pub struct LeftRoom {
221 #[serde(skip_serializing_if = "Timeline::is_empty")]
224 pub timeline: Timeline,
225
226 #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
228 pub state: State,
229
230 #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
232 pub account_data: RoomAccountData,
233}
234
235impl LeftRoom {
236 pub fn new() -> Self {
238 Default::default()
239 }
240
241 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#[derive(Clone, Debug, Default, Serialize)]
250#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
251pub struct JoinedRoom {
252 #[serde(skip_serializing_if = "RoomSummary::is_empty")]
255 pub summary: RoomSummary,
256
257 #[serde(skip_serializing_if = "UnreadNotificationsCount::is_empty")]
265 pub unread_notifications: UnreadNotificationsCount,
266
267 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
276 pub unread_thread_notifications: BTreeMap<OwnedEventId, UnreadNotificationsCount>,
277
278 #[serde(skip_serializing_if = "Timeline::is_empty")]
280 pub timeline: Timeline,
281
282 #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
286 pub state: State,
287
288 #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
290 pub account_data: RoomAccountData,
291
292 #[serde(skip_serializing_if = "Ephemeral::is_empty")]
295 pub ephemeral: Ephemeral,
296
297 #[cfg(feature = "unstable-msc4354")]
301 #[serde(rename = "msc4354_sticky", skip_serializing_if = "Sticky::is_empty")]
302 pub sticky: Sticky,
303
304 #[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 pub fn new() -> Self {
317 Default::default()
318 }
319
320 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
360#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
361pub struct KnockedRoom {
362 #[serde(default, skip_serializing_if = "KnockState::is_empty")]
364 pub knock_state: KnockState,
365}
366
367impl KnockedRoom {
368 pub fn new() -> Self {
370 Default::default()
371 }
372
373 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
388#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
389pub struct KnockState {
390 #[serde(default, skip_serializing_if = "Vec::is_empty")]
392 pub events: Vec<Raw<AnyStrippedStateEvent>>,
393}
394
395impl KnockState {
396 pub fn new() -> Self {
398 Default::default()
399 }
400
401 pub fn is_empty(&self) -> bool {
403 let Self { events } = self;
404 events.is_empty()
405 }
406}
407
408#[derive(Clone, Debug, Default, Deserialize, Serialize)]
410#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
411pub struct Timeline {
412 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
416 pub limited: bool,
417
418 #[serde(skip_serializing_if = "Option::is_none")]
421 pub prev_batch: Option<String>,
422
423 pub events: Vec<Raw<AnySyncTimelineEvent>>,
425}
426
427impl Timeline {
428 pub fn new() -> Self {
430 Default::default()
431 }
432
433 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#[derive(Clone, Debug, Serialize)]
445#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
446pub enum State {
447 #[serde(rename = "state")]
455 Before(StateEvents),
456
457 #[serde(rename = "state_after")]
464 After(StateEvents),
465}
466
467impl State {
468 fn is_before_and_empty(&self) -> bool {
470 as_variant!(self, Self::Before).is_some_and(|state| state.is_empty())
471 }
472
473 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
490#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
491pub struct StateEvents {
492 #[serde(default, skip_serializing_if = "Vec::is_empty")]
494 pub events: Vec<Raw<AnySyncStateEvent>>,
495}
496
497impl StateEvents {
498 pub fn new() -> Self {
500 Default::default()
501 }
502
503 pub fn is_empty(&self) -> bool {
505 let Self { events } = self;
506 events.is_empty()
507 }
508
509 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
523#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
524pub struct GlobalAccountData {
525 #[serde(default, skip_serializing_if = "Vec::is_empty")]
527 pub events: Vec<Raw<AnyGlobalAccountDataEvent>>,
528}
529
530impl GlobalAccountData {
531 pub fn new() -> Self {
533 Default::default()
534 }
535
536 pub fn is_empty(&self) -> bool {
538 let Self { events } = self;
539 events.is_empty()
540 }
541}
542
543#[derive(Clone, Debug, Default, Deserialize, Serialize)]
545#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
546pub struct RoomAccountData {
547 #[serde(default, skip_serializing_if = "Vec::is_empty")]
549 pub events: Vec<Raw<AnyRoomAccountDataEvent>>,
550}
551
552impl RoomAccountData {
553 pub fn new() -> Self {
555 Default::default()
556 }
557
558 pub fn is_empty(&self) -> bool {
560 let Self { events } = self;
561 events.is_empty()
562 }
563}
564
565#[derive(Clone, Debug, Default, Deserialize, Serialize)]
567#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
568pub struct Ephemeral {
569 #[serde(default, skip_serializing_if = "Vec::is_empty")]
571 pub events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
572}
573
574impl Ephemeral {
575 pub fn new() -> Self {
577 Default::default()
578 }
579
580 pub fn is_empty(&self) -> bool {
582 let Self { events } = self;
583 events.is_empty()
584 }
585}
586
587#[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 #[serde(default, skip_serializing_if = "Vec::is_empty")]
596 pub events: Vec<Raw<AnySyncTimelineEvent>>,
597}
598
599#[cfg(feature = "unstable-msc4354")]
600impl Sticky {
601 pub fn new() -> Self {
603 Default::default()
604 }
605
606 pub fn is_empty(&self) -> bool {
608 let Self { events } = self;
609 events.is_empty()
610 }
611}
612
613#[derive(Clone, Debug, Default, Deserialize, Serialize)]
615#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
616pub struct RoomSummary {
617 #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")]
621 pub heroes: Vec<OwnedUserId>,
622
623 #[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
627 pub joined_member_count: Option<UInt>,
628
629 #[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
633 pub invited_member_count: Option<UInt>,
634}
635
636impl RoomSummary {
637 pub fn new() -> Self {
639 Default::default()
640 }
641
642 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
651#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
652pub struct InvitedRoom {
653 #[serde(default, skip_serializing_if = "InviteState::is_empty")]
655 pub invite_state: InviteState,
656}
657
658impl InvitedRoom {
659 pub fn new() -> Self {
661 Default::default()
662 }
663
664 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
679#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
680pub struct InviteState {
681 #[serde(default, skip_serializing_if = "Vec::is_empty")]
683 pub events: Vec<Raw<AnyStrippedStateEvent>>,
684}
685
686impl InviteState {
687 pub fn new() -> Self {
689 Default::default()
690 }
691
692 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#[derive(Clone, Debug, Default, Deserialize, Serialize)]
707#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
708pub struct Presence {
709 #[serde(default, skip_serializing_if = "Vec::is_empty")]
711 pub events: Vec<Raw<PresenceEvent>>,
712}
713
714impl Presence {
715 pub fn new() -> Self {
717 Default::default()
718 }
719
720 pub fn is_empty(&self) -> bool {
722 let Self { events } = self;
723 events.is_empty()
724 }
725}
726
727#[derive(Clone, Debug, Default, Deserialize, Serialize)]
729#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
730pub struct ToDevice {
731 #[serde(default, skip_serializing_if = "Vec::is_empty")]
733 pub events: Vec<Raw<AnyToDeviceEvent>>,
734}
735
736impl ToDevice {
737 pub fn new() -> Self {
739 Default::default()
740 }
741
742 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 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 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 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}