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 #[cfg(feature = "unstable-msc4532")]
76 #[serde(
77 default,
78 skip_serializing_if = "ruma_common::serde::is_default",
79 rename = "org.continuwuity.presence_v2.msc4532.revised_presence"
80 )]
81 #[ruma_api(query)]
82 pub revised_presence: bool,
83
84 #[serde(
86 with = "ruma_common::serde::duration::opt_ms",
87 default,
88 skip_serializing_if = "Option::is_none"
89 )]
90 #[ruma_api(query)]
91 pub timeout: Option<Duration>,
92
93 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
96 #[ruma_api(query)]
97 pub use_state_after: bool,
98}
99
100#[response]
102pub struct Response {
103 pub next_batch: String,
105
106 #[serde(default, skip_serializing_if = "Rooms::is_empty")]
108 pub rooms: Rooms,
109
110 #[serde(default, skip_serializing_if = "Presence::is_empty")]
112 pub presence: Presence,
113
114 #[serde(default, skip_serializing_if = "GlobalAccountData::is_empty")]
116 pub account_data: GlobalAccountData,
117
118 #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
120 pub to_device: ToDevice,
121
122 #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
126 pub device_lists: DeviceLists,
127
128 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
131 pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
138 pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
139}
140
141impl Request {
142 pub fn new() -> Self {
144 Default::default()
145 }
146}
147
148impl Response {
149 pub fn new(next_batch: String) -> Self {
151 Self {
152 next_batch,
153 rooms: Default::default(),
154 presence: Default::default(),
155 account_data: Default::default(),
156 to_device: Default::default(),
157 device_lists: Default::default(),
158 device_one_time_keys_count: BTreeMap::new(),
159 device_unused_fallback_key_types: None,
160 }
161 }
162}
163
164#[derive(Clone, Debug, Deserialize, Serialize)]
166#[allow(clippy::large_enum_variant)]
167#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
168#[serde(untagged)]
169pub enum Filter {
170 #[serde(with = "ruma_common::serde::json_string")]
182 FilterDefinition(FilterDefinition),
183
184 FilterId(String),
186}
187
188impl From<FilterDefinition> for Filter {
189 fn from(def: FilterDefinition) -> Self {
190 Self::FilterDefinition(def)
191 }
192}
193
194impl From<String> for Filter {
195 fn from(id: String) -> Self {
196 Self::FilterId(id)
197 }
198}
199
200#[derive(Clone, Debug, Default, Deserialize, Serialize)]
202#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
203pub struct Rooms {
204 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
206 pub leave: BTreeMap<OwnedRoomId, LeftRoom>,
207
208 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
210 pub join: BTreeMap<OwnedRoomId, JoinedRoom>,
211
212 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
214 pub invite: BTreeMap<OwnedRoomId, InvitedRoom>,
215
216 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
218 pub knock: BTreeMap<OwnedRoomId, KnockedRoom>,
219}
220
221impl Rooms {
222 pub fn new() -> Self {
224 Default::default()
225 }
226
227 pub fn is_empty(&self) -> bool {
229 let Self { leave, join, invite, knock } = self;
230 leave.is_empty() && join.is_empty() && invite.is_empty() && knock.is_empty()
231 }
232}
233
234#[derive(Clone, Debug, Default, Serialize)]
236#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
237pub struct LeftRoom {
238 #[serde(skip_serializing_if = "Timeline::is_empty")]
241 pub timeline: Timeline,
242
243 #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
245 pub state: State,
246
247 #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
249 pub account_data: RoomAccountData,
250}
251
252impl LeftRoom {
253 pub fn new() -> Self {
255 Default::default()
256 }
257
258 pub fn is_empty(&self) -> bool {
260 let Self { timeline, state, account_data } = self;
261 timeline.is_empty() && state.is_empty() && account_data.is_empty()
262 }
263}
264
265#[derive(Clone, Debug, Default, Serialize)]
267#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
268pub struct JoinedRoom {
269 #[serde(skip_serializing_if = "RoomSummary::is_empty")]
272 pub summary: RoomSummary,
273
274 #[serde(skip_serializing_if = "UnreadNotificationsCount::is_empty")]
282 pub unread_notifications: UnreadNotificationsCount,
283
284 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
293 pub unread_thread_notifications: BTreeMap<OwnedEventId, UnreadNotificationsCount>,
294
295 #[serde(skip_serializing_if = "Timeline::is_empty")]
297 pub timeline: Timeline,
298
299 #[serde(flatten, skip_serializing_if = "State::is_before_and_empty")]
303 pub state: State,
304
305 #[serde(skip_serializing_if = "RoomAccountData::is_empty")]
307 pub account_data: RoomAccountData,
308
309 #[serde(skip_serializing_if = "Ephemeral::is_empty")]
312 pub ephemeral: Ephemeral,
313
314 #[cfg(feature = "unstable-msc4354")]
318 #[serde(rename = "msc4354_sticky", skip_serializing_if = "Sticky::is_empty")]
319 pub sticky: Sticky,
320
321 #[cfg(feature = "unstable-msc2654")]
327 #[serde(rename = "org.matrix.msc2654.unread_count", skip_serializing_if = "Option::is_none")]
328 pub unread_count: Option<UInt>,
329}
330
331impl JoinedRoom {
332 pub fn new() -> Self {
334 Default::default()
335 }
336
337 pub fn is_empty(&self) -> bool {
339 let Self {
340 summary,
341 unread_notifications,
342 unread_thread_notifications,
343 timeline,
344 state,
345 account_data,
346 ephemeral,
347 #[cfg(feature = "unstable-msc4354")]
348 sticky,
349 #[cfg(feature = "unstable-msc2654")]
350 unread_count,
351 } = self;
352
353 #[cfg(not(feature = "unstable-msc2654"))]
354 let unread_count_is_none = true;
355 #[cfg(feature = "unstable-msc2654")]
356 let unread_count_is_none = unread_count.is_none();
357
358 #[cfg(not(feature = "unstable-msc4354"))]
359 let sticky_is_empty = true;
360 #[cfg(feature = "unstable-msc4354")]
361 let sticky_is_empty = sticky.is_empty();
362
363 summary.is_empty()
364 && unread_notifications.is_empty()
365 && unread_thread_notifications.is_empty()
366 && timeline.is_empty()
367 && state.is_empty()
368 && account_data.is_empty()
369 && ephemeral.is_empty()
370 && unread_count_is_none
371 && sticky_is_empty
372 }
373}
374
375#[derive(Clone, Debug, Default, Deserialize, Serialize)]
377#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
378pub struct KnockedRoom {
379 #[serde(default, skip_serializing_if = "KnockState::is_empty")]
381 pub knock_state: KnockState,
382}
383
384impl KnockedRoom {
385 pub fn new() -> Self {
387 Default::default()
388 }
389
390 pub fn is_empty(&self) -> bool {
392 let Self { knock_state } = self;
393 knock_state.is_empty()
394 }
395}
396
397impl From<KnockState> for KnockedRoom {
398 fn from(knock_state: KnockState) -> Self {
399 KnockedRoom { knock_state, ..Default::default() }
400 }
401}
402
403#[derive(Clone, Debug, Default, Deserialize, Serialize)]
405#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
406pub struct KnockState {
407 #[serde(default, skip_serializing_if = "Vec::is_empty")]
409 pub events: Vec<Raw<AnyStrippedStateEvent>>,
410}
411
412impl KnockState {
413 pub fn new() -> Self {
415 Default::default()
416 }
417
418 pub fn is_empty(&self) -> bool {
420 let Self { events } = self;
421 events.is_empty()
422 }
423}
424
425#[derive(Clone, Debug, Default, Deserialize, Serialize)]
427#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
428pub struct Timeline {
429 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
433 pub limited: bool,
434
435 #[serde(skip_serializing_if = "Option::is_none")]
438 pub prev_batch: Option<String>,
439
440 pub events: Vec<Raw<AnySyncTimelineEvent>>,
442}
443
444impl Timeline {
445 pub fn new() -> Self {
447 Default::default()
448 }
449
450 pub fn is_empty(&self) -> bool {
455 let Self { limited, prev_batch, events } = self;
456 !limited && prev_batch.is_none() && events.is_empty()
457 }
458}
459
460#[derive(Clone, Debug, Serialize)]
462#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
463pub enum State {
464 #[serde(rename = "state")]
472 Before(StateEvents),
473
474 #[serde(rename = "state_after")]
481 After(StateEvents),
482}
483
484impl State {
485 fn is_before_and_empty(&self) -> bool {
487 as_variant!(self, Self::Before).is_some_and(|state| state.is_empty())
488 }
489
490 pub fn is_empty(&self) -> bool {
492 match self {
493 Self::Before(state) => state.is_empty(),
494 Self::After(state) => state.is_empty(),
495 }
496 }
497}
498
499impl Default for State {
500 fn default() -> Self {
501 Self::Before(Default::default())
502 }
503}
504
505#[derive(Clone, Debug, Default, Deserialize, Serialize)]
507#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
508pub struct StateEvents {
509 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub events: Vec<Raw<AnySyncStateEvent>>,
512}
513
514impl StateEvents {
515 pub fn new() -> Self {
517 Default::default()
518 }
519
520 pub fn is_empty(&self) -> bool {
522 let Self { events } = self;
523 events.is_empty()
524 }
525
526 pub fn with_events(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
528 Self { events, ..Default::default() }
529 }
530}
531
532impl From<Vec<Raw<AnySyncStateEvent>>> for StateEvents {
533 fn from(events: Vec<Raw<AnySyncStateEvent>>) -> Self {
534 Self::with_events(events)
535 }
536}
537
538#[derive(Clone, Debug, Default, Deserialize, Serialize)]
540#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
541pub struct GlobalAccountData {
542 #[serde(default, skip_serializing_if = "Vec::is_empty")]
544 pub events: Vec<Raw<AnyGlobalAccountDataEvent>>,
545}
546
547impl GlobalAccountData {
548 pub fn new() -> Self {
550 Default::default()
551 }
552
553 pub fn is_empty(&self) -> bool {
555 let Self { events } = self;
556 events.is_empty()
557 }
558}
559
560#[derive(Clone, Debug, Default, Deserialize, Serialize)]
562#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
563pub struct RoomAccountData {
564 #[serde(default, skip_serializing_if = "Vec::is_empty")]
566 pub events: Vec<Raw<AnyRoomAccountDataEvent>>,
567}
568
569impl RoomAccountData {
570 pub fn new() -> Self {
572 Default::default()
573 }
574
575 pub fn is_empty(&self) -> bool {
577 let Self { events } = self;
578 events.is_empty()
579 }
580}
581
582#[derive(Clone, Debug, Default, Deserialize, Serialize)]
584#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
585pub struct Ephemeral {
586 #[serde(default, skip_serializing_if = "Vec::is_empty")]
588 pub events: Vec<Raw<AnySyncEphemeralRoomEvent>>,
589}
590
591impl Ephemeral {
592 pub fn new() -> Self {
594 Default::default()
595 }
596
597 pub fn is_empty(&self) -> bool {
599 let Self { events } = self;
600 events.is_empty()
601 }
602}
603
604#[cfg(feature = "unstable-msc4354")]
608#[derive(Clone, Debug, Default, Deserialize, Serialize)]
609#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
610pub struct Sticky {
611 #[serde(default, skip_serializing_if = "Vec::is_empty")]
613 pub events: Vec<Raw<AnySyncTimelineEvent>>,
614}
615
616#[cfg(feature = "unstable-msc4354")]
617impl Sticky {
618 pub fn new() -> Self {
620 Default::default()
621 }
622
623 pub fn is_empty(&self) -> bool {
625 let Self { events } = self;
626 events.is_empty()
627 }
628}
629
630#[derive(Clone, Debug, Default, Deserialize, Serialize)]
632#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
633pub struct RoomSummary {
634 #[serde(rename = "m.heroes", default, skip_serializing_if = "Vec::is_empty")]
638 pub heroes: Vec<OwnedUserId>,
639
640 #[serde(rename = "m.joined_member_count", skip_serializing_if = "Option::is_none")]
644 pub joined_member_count: Option<UInt>,
645
646 #[serde(rename = "m.invited_member_count", skip_serializing_if = "Option::is_none")]
650 pub invited_member_count: Option<UInt>,
651}
652
653impl RoomSummary {
654 pub fn new() -> Self {
656 Default::default()
657 }
658
659 pub fn is_empty(&self) -> bool {
661 let Self { heroes, joined_member_count, invited_member_count } = self;
662 heroes.is_empty() && joined_member_count.is_none() && invited_member_count.is_none()
663 }
664}
665
666#[derive(Clone, Debug, Default, Deserialize, Serialize)]
668#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
669pub struct InvitedRoom {
670 #[serde(default, skip_serializing_if = "InviteState::is_empty")]
672 pub invite_state: InviteState,
673}
674
675impl InvitedRoom {
676 pub fn new() -> Self {
678 Default::default()
679 }
680
681 pub fn is_empty(&self) -> bool {
683 let Self { invite_state } = self;
684 invite_state.is_empty()
685 }
686}
687
688impl From<InviteState> for InvitedRoom {
689 fn from(invite_state: InviteState) -> Self {
690 InvitedRoom { invite_state, ..Default::default() }
691 }
692}
693
694#[derive(Clone, Debug, Default, Deserialize, Serialize)]
696#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
697pub struct InviteState {
698 #[serde(default, skip_serializing_if = "Vec::is_empty")]
700 pub events: Vec<Raw<AnyStrippedStateEvent>>,
701}
702
703impl InviteState {
704 pub fn new() -> Self {
706 Default::default()
707 }
708
709 pub fn is_empty(&self) -> bool {
711 let Self { events } = self;
712 events.is_empty()
713 }
714}
715
716impl From<Vec<Raw<AnyStrippedStateEvent>>> for InviteState {
717 fn from(events: Vec<Raw<AnyStrippedStateEvent>>) -> Self {
718 InviteState { events, ..Default::default() }
719 }
720}
721
722#[derive(Clone, Debug, Default, Deserialize, Serialize)]
724#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
725pub struct Presence {
726 #[serde(default, skip_serializing_if = "Vec::is_empty")]
728 pub events: Vec<Raw<PresenceEvent>>,
729}
730
731impl Presence {
732 pub fn new() -> Self {
734 Default::default()
735 }
736
737 pub fn is_empty(&self) -> bool {
739 let Self { events } = self;
740 events.is_empty()
741 }
742}
743
744#[derive(Clone, Debug, Default, Deserialize, Serialize)]
746#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
747pub struct ToDevice {
748 #[serde(default, skip_serializing_if = "Vec::is_empty")]
750 pub events: Vec<Raw<AnyToDeviceEvent>>,
751}
752
753impl ToDevice {
754 pub fn new() -> Self {
756 Default::default()
757 }
758
759 pub fn is_empty(&self) -> bool {
761 let Self { events } = self;
762 events.is_empty()
763 }
764}
765
766#[cfg(test)]
767mod tests {
768 use assign::assign;
769 use ruma_common::canonical_json::assert_to_canonical_json_eq;
770 use serde_json::{from_value as from_json_value, json};
771
772 use super::Timeline;
773
774 #[test]
775 fn timeline_serde() {
776 let timeline = assign!(Timeline::new(), { limited: true });
777 let timeline_serialized = json!({ "events": [], "limited": true });
778 assert_to_canonical_json_eq!(timeline, timeline_serialized.clone());
779
780 let timeline_deserialized = from_json_value::<Timeline>(timeline_serialized).unwrap();
781 assert!(timeline_deserialized.limited);
782
783 let timeline_default = Timeline::default();
784 assert_to_canonical_json_eq!(timeline_default, json!({ "events": [] }));
785
786 let timeline_default_deserialized =
787 from_json_value::<Timeline>(json!({ "events": [] })).unwrap();
788 assert!(!timeline_default_deserialized.limited);
789 }
790
791 #[cfg(feature = "unstable-msc4354")]
792 #[test]
793 fn joined_room_sticky_section_serde() {
794 use ruma_events::{AnySyncMessageLikeEvent, AnySyncTimelineEvent, SyncMessageLikeEvent};
795 use serde_json::to_value as to_json_value;
796 use strass::assert_let;
797
798 use super::JoinedRoom;
799
800 let sticky_event = json!({
801 "content": { "body": "sticky", "msgtype": "m.text" },
802 "event_id": "$1:example.com",
803 "origin_server_ts": 1,
804 "sender": "@alice:example.com",
805 "type": "m.room.message",
806 "msc4354_sticky": {
807 "duration_ms": 300_000
808 },
809 "unsigned": { "sticky_duration_ttl_ms": 258_113 }
810 });
811
812 let joined_room = from_json_value::<JoinedRoom>(json!({
814 "msc4354_sticky": { "events": [sticky_event] }
815 }))
816 .unwrap();
817 assert_eq!(joined_room.sticky.events.len(), 1);
818
819 let joined_room = from_json_value::<JoinedRoom>(json!({
822 "sticky": { "events": [] },
823 "msc4354_sticky": { "events": [sticky_event] },
824 }))
825 .unwrap();
826 assert_eq!(joined_room.sticky.events.len(), 1);
827 let event_raw = joined_room.sticky.events.first().unwrap();
828
829 assert_let!(
830 AnySyncTimelineEvent::MessageLike(AnySyncMessageLikeEvent::RoomMessage(
831 SyncMessageLikeEvent::Original(ev)
832 )) = event_raw.deserialize().unwrap()
833 );
834
835 let duration = ev.sticky.map(|s| s.duration_ms.get());
836 assert_eq!(duration, Some(300_000));
837
838 let serialized = to_json_value(&joined_room).unwrap();
840 assert!(serialized.get("msc4354_sticky").is_some());
841 assert!(serialized.get("sticky").is_none());
842 }
843}
844
845#[cfg(all(test, feature = "client"))]
846mod client_tests {
847 use std::{borrow::Cow, time::Duration};
848
849 use ruma_common::{
850 RoomVersionId,
851 api::{
852 IncomingResponseExt as _, MatrixVersion, OutgoingRequestExt as _, SupportedVersions,
853 auth_scheme::SendAccessToken,
854 },
855 event_id, room_id, user_id,
856 };
857 use ruma_events::AnyStrippedStateEvent;
858 use serde_json::{Value as JsonValue, json};
859 use strass::assert_let;
860
861 use super::{Filter, PresenceState, Request, Response, State};
862
863 fn sync_state_event() -> JsonValue {
864 json!({
865 "content": {
866 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
867 "displayname": "Alice Margatroid",
868 "membership": "join",
869 },
870 "event_id": "$143273582443PhrSn",
871 "origin_server_ts": 1_432_735_824,
872 "sender": "@alice:example.org",
873 "state_key": "@alice:example.org",
874 "type": "m.room.member",
875 "unsigned": {
876 "age": 1234,
877 "membership": "join",
878 },
879 })
880 }
881
882 #[test]
883 fn serialize_request_all_params() {
884 let supported = SupportedVersions {
885 versions: [MatrixVersion::V1_1].into(),
886 features: Default::default(),
887 };
888 let req: http::Request<Vec<u8>> = Request {
889 filter: Some(Filter::FilterId("66696p746572".to_owned())),
890 since: Some("s72594_4483_1934".to_owned()),
891 full_state: true,
892 set_presence: PresenceState::Offline,
893 timeout: Some(Duration::from_millis(30000)),
894 use_state_after: true,
895 #[cfg(feature = "unstable-msc4532")]
896 revised_presence: true,
897 }
898 .try_into_http_request(
899 "https://homeserver.tld",
900 SendAccessToken::IfRequired("auth_tok"),
901 Cow::Owned(supported),
902 )
903 .unwrap();
904
905 let uri = req.uri();
906 let query = uri.query().unwrap();
907
908 assert_eq!(uri.path(), "/_matrix/client/v3/sync");
909 assert!(query.contains("filter=66696p746572"));
910 assert!(query.contains("since=s72594_4483_1934"));
911 assert!(query.contains("full_state=true"));
912 assert!(query.contains("set_presence=offline"));
913 assert!(query.contains("timeout=30000"));
914 assert!(query.contains("use_state_after=true"));
915 #[cfg(feature = "unstable-msc4532")]
916 assert!(query.contains("revised_presence=true"));
917 }
918
919 #[test]
920 fn deserialize_response_invite() {
921 let creator = user_id!("@creator:localhost");
922 let invitee = user_id!("@invitee:localhost");
923 let room_id = room_id!("!privateroom:localhost");
924 let event_id = event_id!("$invite");
925
926 let body = json!({
927 "next_batch": "a00",
928 "rooms": {
929 "invite": {
930 room_id: {
931 "invite_state": {
932 "events": [
933 {
934 "content": {
935 "room_version": "11",
936 },
937 "type": "m.room.create",
938 "state_key": "",
939 "sender": creator,
940 },
941 {
942 "content": {
943 "membership": "invite",
944 },
945 "type": "m.room.member",
946 "state_key": invitee,
947 "sender": creator,
948 "origin_server_ts": 4_345_456,
949 "event_id": event_id,
950 },
951 ],
952 },
953 },
954 },
955 },
956 })
957 .to_string();
958 let http_response = http::Response::new(body.as_bytes());
959
960 let response = Response::try_from_http_response(http_response).unwrap();
961 assert_eq!(response.next_batch, "a00");
962 let private_room = response.rooms.invite.get(room_id).unwrap();
963
964 let first_event = private_room.invite_state.events[0].deserialize().unwrap();
965 assert_let!(AnyStrippedStateEvent::RoomCreate(create_event) = first_event);
966 assert_eq!(create_event.sender, creator);
967 assert_eq!(create_event.content.room_version, RoomVersionId::V11);
968 }
969
970 #[test]
971 fn deserialize_response_no_state() {
972 let joined_room_id = room_id!("!joined:localhost");
973 let left_room_id = room_id!("!left:localhost");
974 let event = sync_state_event();
975
976 let body = json!({
977 "next_batch": "aaa",
978 "rooms": {
979 "join": {
980 joined_room_id: {
981 "timeline": {
982 "events": [
983 event,
984 ],
985 },
986 },
987 },
988 "leave": {
989 left_room_id: {
990 "timeline": {
991 "events": [
992 event,
993 ],
994 },
995 },
996 },
997 },
998 })
999 .to_string();
1000
1001 let http_response = http::Response::new(body.as_bytes());
1002
1003 let response = Response::try_from_http_response(http_response).unwrap();
1004 assert_eq!(response.next_batch, "aaa");
1005
1006 let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1007 assert_eq!(joined_room.timeline.events.len(), 1);
1008 assert!(joined_room.state.is_before_and_empty());
1009
1010 let left_room = response.rooms.leave.get(left_room_id).unwrap();
1011 assert_eq!(left_room.timeline.events.len(), 1);
1012 assert!(left_room.state.is_before_and_empty());
1013 }
1014
1015 #[test]
1016 fn deserialize_response_state_before() {
1017 let joined_room_id = room_id!("!joined:localhost");
1018 let left_room_id = room_id!("!left:localhost");
1019 let event = sync_state_event();
1020
1021 let body = json!({
1022 "next_batch": "aaa",
1023 "rooms": {
1024 "join": {
1025 joined_room_id: {
1026 "state": {
1027 "events": [
1028 event,
1029 ],
1030 },
1031 },
1032 },
1033 "leave": {
1034 left_room_id: {
1035 "state": {
1036 "events": [
1037 event,
1038 ],
1039 },
1040 },
1041 },
1042 },
1043 })
1044 .to_string();
1045
1046 let http_response = http::Response::new(body.as_bytes());
1047
1048 let response = Response::try_from_http_response(http_response).unwrap();
1049 assert_eq!(response.next_batch, "aaa");
1050
1051 let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1052 assert!(joined_room.timeline.is_empty());
1053 assert_let!(State::Before(state) = &joined_room.state);
1054 assert_eq!(state.events.len(), 1);
1055
1056 let left_room = response.rooms.leave.get(left_room_id).unwrap();
1057 assert!(left_room.timeline.is_empty());
1058 assert_let!(State::Before(state) = &left_room.state);
1059 assert_eq!(state.events.len(), 1);
1060 }
1061
1062 #[test]
1063 fn deserialize_response_empty_state_after() {
1064 let joined_room_id = room_id!("!joined:localhost");
1065 let left_room_id = room_id!("!left:localhost");
1066
1067 let body = json!({
1068 "next_batch": "aaa",
1069 "rooms": {
1070 "join": {
1071 joined_room_id: {
1072 "state_after": {},
1073 },
1074 },
1075 "leave": {
1076 left_room_id: {
1077 "state_after": {},
1078 },
1079 },
1080 },
1081 })
1082 .to_string();
1083
1084 let http_response = http::Response::new(body.as_bytes());
1085
1086 let response = Response::try_from_http_response(http_response).unwrap();
1087 assert_eq!(response.next_batch, "aaa");
1088
1089 let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1090 assert!(joined_room.timeline.is_empty());
1091 assert_let!(State::After(state) = &joined_room.state);
1092 assert_eq!(state.events.len(), 0);
1093
1094 let left_room = response.rooms.leave.get(left_room_id).unwrap();
1095 assert!(left_room.timeline.is_empty());
1096 assert_let!(State::After(state) = &left_room.state);
1097 assert_eq!(state.events.len(), 0);
1098 }
1099
1100 #[test]
1101 fn deserialize_response_non_empty_state_after() {
1102 let joined_room_id = room_id!("!joined:localhost");
1103 let left_room_id = room_id!("!left:localhost");
1104 let event = sync_state_event();
1105
1106 let body = json!({
1107 "next_batch": "aaa",
1108 "rooms": {
1109 "join": {
1110 joined_room_id: {
1111 "state_after": {
1112 "events": [
1113 event,
1114 ],
1115 },
1116 },
1117 },
1118 "leave": {
1119 left_room_id: {
1120 "state_after": {
1121 "events": [
1122 event,
1123 ],
1124 },
1125 },
1126 },
1127 },
1128 })
1129 .to_string();
1130
1131 let http_response = http::Response::new(body.as_bytes());
1132
1133 let response = Response::try_from_http_response(http_response).unwrap();
1134 assert_eq!(response.next_batch, "aaa");
1135
1136 let joined_room = response.rooms.join.get(joined_room_id).unwrap();
1137 assert!(joined_room.timeline.is_empty());
1138 assert_let!(State::After(state) = &joined_room.state);
1139 assert_eq!(state.events.len(), 1);
1140
1141 let left_room = response.rooms.leave.get(left_room_id).unwrap();
1142 assert!(left_room.timeline.is_empty());
1143 assert_let!(State::After(state) = &left_room.state);
1144 assert_eq!(state.events.len(), 1);
1145 }
1146}
1147
1148#[cfg(all(test, feature = "server"))]
1149mod server_tests {
1150 use std::time::Duration;
1151
1152 use assert_matches::assert_matches;
1153 use ruma_common::{
1154 api::{IncomingRequestExt as _, OutgoingResponseExt as _},
1155 owned_room_id,
1156 presence::PresenceState,
1157 serde::Raw,
1158 };
1159 use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent};
1160 use serde_json::{Value as JsonValue, from_slice as from_json_slice, json};
1161 use strass::assert_let;
1162
1163 use super::{Filter, JoinedRoom, KnockedRoom, LeftRoom, Request, Response, State};
1164
1165 fn sync_state_event() -> Raw<AnySyncStateEvent> {
1166 Raw::new(&json!({
1167 "content": {
1168 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1169 "displayname": "Alice Margatroid",
1170 "membership": "join",
1171 },
1172 "event_id": "$143273582443PhrSn",
1173 "origin_server_ts": 1_432_735_824,
1174 "sender": "@alice:example.org",
1175 "state_key": "@alice:example.org",
1176 "type": "m.room.member",
1177 "unsigned": {
1178 "age": 1234,
1179 "membership": "join",
1180 },
1181 }))
1182 .unwrap()
1183 .cast_unchecked()
1184 }
1185
1186 #[test]
1187 fn deserialize_request_all_query_params() {
1188 let uri = http::Uri::builder()
1189 .scheme("https")
1190 .authority("matrix.org")
1191 .path_and_query(
1192 "/_matrix/client/r0/sync\
1193 ?filter=myfilter\
1194 &since=myts\
1195 &full_state=false\
1196 &set_presence=offline\
1197 &timeout=5000",
1198 )
1199 .build()
1200 .unwrap();
1201
1202 let req = Request::try_from_http_request(
1203 http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1204 &[],
1205 )
1206 .unwrap();
1207
1208 assert_let!(Some(Filter::FilterId(id)) = req.filter);
1209 assert_eq!(id, "myfilter");
1210 assert_eq!(req.since.as_deref(), Some("myts"));
1211 assert!(!req.full_state);
1212 assert_eq!(req.set_presence, PresenceState::Offline);
1213 assert_eq!(req.timeout, Some(Duration::from_millis(5000)));
1214 }
1215
1216 #[test]
1217 #[allow(deprecated)]
1218 fn deserialize_request_no_query_params() {
1219 let uri = http::Uri::builder()
1220 .scheme("https")
1221 .authority("matrix.org")
1222 .path_and_query("/_matrix/client/r0/sync")
1223 .build()
1224 .unwrap();
1225
1226 let req = Request::try_from_http_request(
1227 http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1228 &[],
1229 )
1230 .unwrap();
1231
1232 assert_matches!(req.filter, None);
1233 assert_eq!(req.since, None);
1234 assert!(!req.full_state);
1235 #[cfg(not(feature = "unstable-msc4532"))]
1236 assert_eq!(req.set_presence, PresenceState::Online);
1237 #[cfg(feature = "unstable-msc4532")]
1238 assert_eq!(req.set_presence, PresenceState::Active);
1239 assert_eq!(req.timeout, None);
1240 }
1241
1242 #[test]
1243 #[allow(deprecated)]
1244 fn deserialize_request_some_query_params() {
1245 let uri = http::Uri::builder()
1246 .scheme("https")
1247 .authority("matrix.org")
1248 .path_and_query(
1249 "/_matrix/client/r0/sync\
1250 ?filter=EOKFFmdZYF\
1251 &timeout=0",
1252 )
1253 .build()
1254 .unwrap();
1255
1256 let req = Request::try_from_http_request(
1257 http::Request::builder().uri(uri).body(&[] as &[u8]).unwrap(),
1258 &[],
1259 )
1260 .unwrap();
1261
1262 assert_let!(Some(Filter::FilterId(id)) = req.filter);
1263 assert_eq!(id, "EOKFFmdZYF");
1264 assert_eq!(req.since, None);
1265 assert!(!req.full_state);
1266 #[cfg(not(feature = "unstable-msc4532"))]
1267 assert_eq!(req.set_presence, PresenceState::Online);
1268 #[cfg(feature = "unstable-msc4532")]
1269 assert_eq!(req.set_presence, PresenceState::Active);
1270 assert_eq!(req.timeout, Some(Duration::from_millis(0)));
1271 }
1272
1273 #[test]
1274 fn serialize_response_no_state() {
1275 let joined_room_id = owned_room_id!("!joined:localhost");
1276 let left_room_id = owned_room_id!("!left:localhost");
1277 let event = sync_state_event();
1278
1279 let mut response = Response::new("aaa".to_owned());
1280
1281 let mut joined_room = JoinedRoom::new();
1282 joined_room.timeline.events.push(event.clone().cast());
1283 response.rooms.join.insert(joined_room_id.clone(), joined_room);
1284
1285 let mut left_room = LeftRoom::new();
1286 left_room.timeline.events.push(event.clone().cast());
1287 response.rooms.leave.insert(left_room_id.clone(), left_room);
1288
1289 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1290
1291 assert_eq!(
1292 from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1293 json!({
1294 "next_batch": "aaa",
1295 "rooms": {
1296 "join": {
1297 joined_room_id: {
1298 "timeline": {
1299 "events": [
1300 event,
1301 ],
1302 },
1303 },
1304 },
1305 "leave": {
1306 left_room_id: {
1307 "timeline": {
1308 "events": [
1309 event,
1310 ],
1311 },
1312 },
1313 },
1314 },
1315 })
1316 );
1317 }
1318
1319 #[test]
1320 fn serialize_response_state_before() {
1321 let joined_room_id = owned_room_id!("!joined:localhost");
1322 let left_room_id = owned_room_id!("!left:localhost");
1323 let event = sync_state_event();
1324
1325 let mut response = Response::new("aaa".to_owned());
1326
1327 let mut joined_room = JoinedRoom::new();
1328 joined_room.state = State::Before(vec![event.clone()].into());
1329 response.rooms.join.insert(joined_room_id.clone(), joined_room);
1330
1331 let mut left_room = LeftRoom::new();
1332 left_room.state = State::Before(vec![event.clone()].into());
1333 response.rooms.leave.insert(left_room_id.clone(), left_room);
1334
1335 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1336
1337 assert_eq!(
1338 from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1339 json!({
1340 "next_batch": "aaa",
1341 "rooms": {
1342 "join": {
1343 joined_room_id: {
1344 "state": {
1345 "events": [
1346 event,
1347 ],
1348 },
1349 },
1350 },
1351 "leave": {
1352 left_room_id: {
1353 "state": {
1354 "events": [
1355 event,
1356 ],
1357 },
1358 },
1359 },
1360 },
1361 })
1362 );
1363 }
1364
1365 #[test]
1366 fn serialize_response_empty_state_after() {
1367 let joined_room_id = owned_room_id!("!joined:localhost");
1368 let left_room_id = owned_room_id!("!left:localhost");
1369
1370 let mut response = Response::new("aaa".to_owned());
1371
1372 let mut joined_room = JoinedRoom::new();
1373 joined_room.state = State::After(Default::default());
1374 response.rooms.join.insert(joined_room_id.clone(), joined_room);
1375
1376 let mut left_room = LeftRoom::new();
1377 left_room.state = State::After(Default::default());
1378 response.rooms.leave.insert(left_room_id.clone(), left_room);
1379
1380 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1381
1382 assert_eq!(
1383 from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1384 json!({
1385 "next_batch": "aaa",
1386 "rooms": {
1387 "join": {
1388 joined_room_id: {
1389 "state_after": {},
1390 },
1391 },
1392 "leave": {
1393 left_room_id: {
1394 "state_after": {},
1395 },
1396 },
1397 },
1398 })
1399 );
1400 }
1401
1402 #[test]
1403 fn serialize_response_non_empty_state_after() {
1404 let joined_room_id = owned_room_id!("!joined:localhost");
1405 let left_room_id = owned_room_id!("!left:localhost");
1406 let event = sync_state_event();
1407
1408 let mut response = Response::new("aaa".to_owned());
1409
1410 let mut joined_room = JoinedRoom::new();
1411 joined_room.state = State::After(vec![event.clone()].into());
1412 response.rooms.join.insert(joined_room_id.clone(), joined_room);
1413
1414 let mut left_room = LeftRoom::new();
1415 left_room.state = State::After(vec![event.clone()].into());
1416 response.rooms.leave.insert(left_room_id.clone(), left_room);
1417
1418 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1419
1420 assert_eq!(
1421 from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1422 json!({
1423 "next_batch": "aaa",
1424 "rooms": {
1425 "join": {
1426 joined_room_id: {
1427 "state_after": {
1428 "events": [
1429 event,
1430 ],
1431 },
1432 },
1433 },
1434 "leave": {
1435 left_room_id: {
1436 "state_after": {
1437 "events": [
1438 event,
1439 ],
1440 },
1441 },
1442 },
1443 },
1444 })
1445 );
1446 }
1447
1448 #[test]
1449 fn serialize_response_knocked_room() {
1450 let knocked_room_id = owned_room_id!("!knocked:localhost");
1451 let event: Raw<AnyStrippedStateEvent> = Raw::new(&json!({
1452 "content": {
1453 "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF",
1454 "displayname": "Alice Margatroid",
1455 "membership": "join",
1456 },
1457 "sender": "@alice:example.org",
1458 "state_key": "@alice:example.org",
1459 "type": "m.room.member",
1460 }))
1461 .unwrap()
1462 .cast_unchecked();
1463
1464 let mut response = Response::new("aaa".to_owned());
1465
1466 let mut knocked_room = KnockedRoom::new();
1467 knocked_room.knock_state.events.push(event.clone());
1468 response.rooms.knock.insert(knocked_room_id.clone(), knocked_room);
1469
1470 let http_response = response.try_into_http_response::<Vec<u8>>().unwrap();
1471
1472 assert_eq!(
1473 from_json_slice::<JsonValue>(http_response.body()).unwrap(),
1474 json!({
1475 "next_batch": "aaa",
1476 "rooms": {
1477 "knock": {
1478 knocked_room_id: {
1479 "knock_state": {
1480 "events": [
1481 event,
1482 ],
1483 },
1484 },
1485 },
1486 },
1487 })
1488 );
1489 }
1490}