1use std::{collections::BTreeMap, time::Duration};
11
12use js_int::UInt;
13use js_option::JsOption;
14use ruma_common::{
15 api::{request, response, Metadata},
16 metadata,
17 serde::{duration::opt_ms, Raw},
18 OwnedMxcUri, OwnedRoomId, OwnedUserId,
19};
20use ruma_events::{AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType};
21use serde::{Deserialize, Serialize};
22
23#[cfg(feature = "unstable-msc3575")]
24use super::v4;
25use super::UnreadNotificationsCount;
26
27const METADATA: Metadata = metadata! {
28 method: POST,
29 rate_limited: false,
30 authentication: AccessToken,
31 history: {
32 unstable => "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync",
33 }
35};
36
37#[request(error = crate::Error)]
39#[derive(Default)]
40pub struct Request {
41 #[serde(skip_serializing_if = "Option::is_none")]
47 #[ruma_api(query)]
48 pub pos: Option<String>,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
61 pub conn_id: Option<String>,
62
63 #[serde(skip_serializing_if = "Option::is_none")]
66 pub txn_id: Option<String>,
67
68 #[serde(with = "opt_ms", default, skip_serializing_if = "Option::is_none")]
72 #[ruma_api(query)]
73 pub timeout: Option<Duration>,
74
75 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
77 pub lists: BTreeMap<String, request::List>,
78
79 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
84 pub room_subscriptions: BTreeMap<OwnedRoomId, request::RoomSubscription>,
85
86 #[serde(default, skip_serializing_if = "request::Extensions::is_empty")]
88 pub extensions: request::Extensions,
89}
90
91impl Request {
92 pub fn new() -> Self {
94 Default::default()
95 }
96}
97
98pub mod request {
100 use ruma_common::{directory::RoomTypeFilter, serde::deserialize_cow_str, RoomId};
101 use serde::de::Error as _;
102
103 use super::{BTreeMap, Deserialize, OwnedRoomId, Serialize, StateEventType, UInt};
104
105 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
107 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
108 pub struct List {
109 pub ranges: Vec<(UInt, UInt)>,
111
112 #[serde(flatten)]
114 pub room_details: RoomDetails,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
119 pub include_heroes: Option<bool>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub filters: Option<ListFilters>,
124 }
125
126 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
131 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
132 pub struct ListFilters {
133 #[serde(skip_serializing_if = "Option::is_none")]
140 pub is_invite: Option<bool>,
141
142 #[serde(default, skip_serializing_if = "<[_]>::is_empty")]
146 pub not_room_types: Vec<RoomTypeFilter>,
147 }
148
149 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
151 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
152 pub struct RoomSubscription {
153 #[serde(default, skip_serializing_if = "Vec::is_empty")]
156 pub required_state: Vec<(StateEventType, String)>,
157
158 pub timeline_limit: UInt,
160
161 #[serde(skip_serializing_if = "Option::is_none")]
163 pub include_heroes: Option<bool>,
164 }
165
166 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
168 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
169 pub struct RoomDetails {
170 #[serde(default, skip_serializing_if = "Vec::is_empty")]
172 pub required_state: Vec<(StateEventType, String)>,
173
174 pub timeline_limit: UInt,
176 }
177
178 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
180 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
181 pub struct Extensions {
182 #[serde(default, skip_serializing_if = "ToDevice::is_empty")]
184 pub to_device: ToDevice,
185
186 #[serde(default, skip_serializing_if = "E2EE::is_empty")]
188 pub e2ee: E2EE,
189
190 #[serde(default, skip_serializing_if = "AccountData::is_empty")]
192 pub account_data: AccountData,
193
194 #[serde(default, skip_serializing_if = "Receipts::is_empty")]
196 pub receipts: Receipts,
197
198 #[serde(default, skip_serializing_if = "Typing::is_empty")]
200 pub typing: Typing,
201
202 #[serde(flatten)]
204 other: BTreeMap<String, serde_json::Value>,
205 }
206
207 impl Extensions {
208 pub fn is_empty(&self) -> bool {
210 self.to_device.is_empty()
211 && self.e2ee.is_empty()
212 && self.account_data.is_empty()
213 && self.receipts.is_empty()
214 && self.typing.is_empty()
215 && self.other.is_empty()
216 }
217 }
218
219 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
223 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
224 pub struct ToDevice {
225 #[serde(skip_serializing_if = "Option::is_none")]
227 pub enabled: Option<bool>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub limit: Option<UInt>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub since: Option<String>,
236
237 #[serde(skip_serializing_if = "Option::is_none")]
242 pub lists: Option<Vec<String>>,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
250 pub rooms: Option<Vec<OwnedRoomId>>,
251 }
252
253 impl ToDevice {
254 pub fn is_empty(&self) -> bool {
256 self.enabled.is_none() && self.limit.is_none() && self.since.is_none()
257 }
258 }
259
260 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
264 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
265 pub struct E2EE {
266 #[serde(skip_serializing_if = "Option::is_none")]
268 pub enabled: Option<bool>,
269 }
270
271 impl E2EE {
272 pub fn is_empty(&self) -> bool {
274 self.enabled.is_none()
275 }
276 }
277
278 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
283 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
284 pub struct AccountData {
285 #[serde(skip_serializing_if = "Option::is_none")]
287 pub enabled: Option<bool>,
288
289 #[serde(skip_serializing_if = "Option::is_none")]
296 pub lists: Option<Vec<String>>,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
306 pub rooms: Option<Vec<OwnedRoomId>>,
307 }
308
309 impl AccountData {
310 pub fn is_empty(&self) -> bool {
312 self.enabled.is_none()
313 }
314 }
315
316 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
320 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
321 pub struct Receipts {
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub enabled: Option<bool>,
325
326 #[serde(skip_serializing_if = "Option::is_none")]
331 pub lists: Option<Vec<String>>,
332
333 #[serde(skip_serializing_if = "Option::is_none")]
339 pub rooms: Option<Vec<ReceiptsRoom>>,
340 }
341
342 impl Receipts {
343 pub fn is_empty(&self) -> bool {
345 self.enabled.is_none()
346 }
347 }
348
349 #[derive(Clone, Debug, PartialEq)]
352 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
353 pub enum ReceiptsRoom {
354 AllSubscribed,
356
357 Room(OwnedRoomId),
359 }
360
361 impl Serialize for ReceiptsRoom {
362 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
363 where
364 S: serde::Serializer,
365 {
366 match self {
367 Self::AllSubscribed => serializer.serialize_str("*"),
368 Self::Room(r) => r.serialize(serializer),
369 }
370 }
371 }
372
373 impl<'de> Deserialize<'de> for ReceiptsRoom {
374 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
375 where
376 D: serde::de::Deserializer<'de>,
377 {
378 match deserialize_cow_str(deserializer)?.as_ref() {
379 "*" => Ok(Self::AllSubscribed),
380 other => Ok(Self::Room(RoomId::parse(other).map_err(D::Error::custom)?.to_owned())),
381 }
382 }
383 }
384
385 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
390 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
391 pub struct Typing {
392 #[serde(skip_serializing_if = "Option::is_none")]
394 pub enabled: Option<bool>,
395
396 #[serde(skip_serializing_if = "Option::is_none")]
401 pub lists: Option<Vec<String>>,
402
403 #[serde(skip_serializing_if = "Option::is_none")]
409 pub rooms: Option<Vec<OwnedRoomId>>,
410 }
411
412 impl Typing {
413 pub fn is_empty(&self) -> bool {
415 self.enabled.is_none()
416 }
417 }
418}
419
420#[response(error = crate::Error)]
422pub struct Response {
423 #[serde(skip_serializing_if = "Option::is_none")]
425 pub txn_id: Option<String>,
426
427 pub pos: String,
430
431 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
433 pub lists: BTreeMap<String, response::List>,
434
435 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
437 pub rooms: BTreeMap<OwnedRoomId, response::Room>,
438
439 #[serde(default, skip_serializing_if = "response::Extensions::is_empty")]
441 pub extensions: response::Extensions,
442}
443
444impl Response {
445 pub fn new(pos: String) -> Self {
447 Self {
448 txn_id: None,
449 pos,
450 lists: Default::default(),
451 rooms: Default::default(),
452 extensions: Default::default(),
453 }
454 }
455}
456
457pub mod response {
459 use ruma_common::OneTimeKeyAlgorithm;
460 use ruma_events::{
461 receipt::SyncReceiptEvent, typing::SyncTypingEvent, AnyGlobalAccountDataEvent,
462 AnyRoomAccountDataEvent, AnyToDeviceEvent,
463 };
464
465 use super::{
466 super::DeviceLists, AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent,
467 BTreeMap, Deserialize, JsOption, OwnedMxcUri, OwnedRoomId, OwnedUserId, Raw, Serialize,
468 UInt, UnreadNotificationsCount,
469 };
470
471 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
474 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
475 pub struct List {
476 pub count: UInt,
478 }
479
480 #[derive(Clone, Debug, Default, Deserialize, Serialize)]
482 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
483 pub struct Room {
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub name: Option<String>,
487
488 #[serde(default, skip_serializing_if = "JsOption::is_undefined")]
490 pub avatar: JsOption<OwnedMxcUri>,
491
492 #[serde(skip_serializing_if = "Option::is_none")]
494 pub initial: Option<bool>,
495
496 #[serde(skip_serializing_if = "Option::is_none")]
498 pub is_dm: Option<bool>,
499
500 #[serde(skip_serializing_if = "Option::is_none")]
503 pub invite_state: Option<Vec<Raw<AnyStrippedStateEvent>>>,
504
505 #[serde(flatten, default, skip_serializing_if = "UnreadNotificationsCount::is_empty")]
507 pub unread_notifications: UnreadNotificationsCount,
508
509 #[serde(default, skip_serializing_if = "Vec::is_empty")]
511 pub timeline: Vec<Raw<AnySyncTimelineEvent>>,
512
513 #[serde(default, skip_serializing_if = "Vec::is_empty")]
515 pub required_state: Vec<Raw<AnySyncStateEvent>>,
516
517 #[serde(skip_serializing_if = "Option::is_none")]
520 pub prev_batch: Option<String>,
521
522 #[serde(default, skip_serializing_if = "ruma_common::serde::is_default")]
525 pub limited: bool,
526
527 #[serde(skip_serializing_if = "Option::is_none")]
530 pub joined_count: Option<UInt>,
531
532 #[serde(skip_serializing_if = "Option::is_none")]
534 pub invited_count: Option<UInt>,
535
536 #[serde(skip_serializing_if = "Option::is_none")]
539 pub num_live: Option<UInt>,
540
541 #[serde(skip_serializing_if = "Option::is_none")]
548 pub bump_stamp: Option<UInt>,
549
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub heroes: Option<Vec<Hero>>,
553 }
554
555 impl Room {
556 pub fn new() -> Self {
558 Default::default()
559 }
560 }
561
562 #[derive(Clone, Debug, Deserialize, Serialize)]
564 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
565 pub struct Hero {
566 pub user_id: OwnedUserId,
568
569 #[serde(rename = "displayname", skip_serializing_if = "Option::is_none")]
571 pub name: Option<String>,
572
573 #[serde(rename = "avatar_url", skip_serializing_if = "Option::is_none")]
575 pub avatar: Option<OwnedMxcUri>,
576 }
577
578 impl Hero {
579 pub fn new(user_id: OwnedUserId) -> Self {
581 Self { user_id, name: None, avatar: None }
582 }
583 }
584
585 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
587 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
588 pub struct Extensions {
589 #[serde(skip_serializing_if = "Option::is_none")]
591 pub to_device: Option<ToDevice>,
592
593 #[serde(default, skip_serializing_if = "E2EE::is_empty")]
595 pub e2ee: E2EE,
596
597 #[serde(default, skip_serializing_if = "AccountData::is_empty")]
599 pub account_data: AccountData,
600
601 #[serde(default, skip_serializing_if = "Receipts::is_empty")]
603 pub receipts: Receipts,
604
605 #[serde(default, skip_serializing_if = "Typing::is_empty")]
607 pub typing: Typing,
608 }
609
610 impl Extensions {
611 pub fn is_empty(&self) -> bool {
615 self.to_device.is_none()
616 && self.e2ee.is_empty()
617 && self.account_data.is_empty()
618 && self.receipts.is_empty()
619 && self.typing.is_empty()
620 }
621 }
622
623 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
627 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
628 pub struct ToDevice {
629 pub next_batch: String,
631
632 #[serde(default, skip_serializing_if = "Vec::is_empty")]
634 pub events: Vec<Raw<AnyToDeviceEvent>>,
635 }
636
637 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
641 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
642 pub struct E2EE {
643 #[serde(default, skip_serializing_if = "DeviceLists::is_empty")]
645 pub device_lists: DeviceLists,
646
647 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
650 pub device_one_time_keys_count: BTreeMap<OneTimeKeyAlgorithm, UInt>,
651
652 #[serde(skip_serializing_if = "Option::is_none")]
657 pub device_unused_fallback_key_types: Option<Vec<OneTimeKeyAlgorithm>>,
658 }
659
660 impl E2EE {
661 pub fn is_empty(&self) -> bool {
663 self.device_lists.is_empty()
664 && self.device_one_time_keys_count.is_empty()
665 && self.device_unused_fallback_key_types.is_none()
666 }
667 }
668
669 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
674 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
675 pub struct AccountData {
676 #[serde(default, skip_serializing_if = "Vec::is_empty")]
678 pub global: Vec<Raw<AnyGlobalAccountDataEvent>>,
679
680 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
682 pub rooms: BTreeMap<OwnedRoomId, Vec<Raw<AnyRoomAccountDataEvent>>>,
683 }
684
685 impl AccountData {
686 pub fn is_empty(&self) -> bool {
688 self.global.is_empty() && self.rooms.is_empty()
689 }
690 }
691
692 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
696 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
697 pub struct Receipts {
698 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
700 pub rooms: BTreeMap<OwnedRoomId, Raw<SyncReceiptEvent>>,
701 }
702
703 impl Receipts {
704 pub fn is_empty(&self) -> bool {
706 self.rooms.is_empty()
707 }
708 }
709
710 #[derive(Clone, Debug, Default, Serialize, Deserialize)]
715 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
716 pub struct Typing {
717 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
719 pub rooms: BTreeMap<OwnedRoomId, Raw<SyncTypingEvent>>,
720 }
721
722 impl Typing {
723 pub fn is_empty(&self) -> bool {
725 self.rooms.is_empty()
726 }
727 }
728}
729
730#[cfg(feature = "unstable-msc3575")]
731impl From<v4::Response> for Response {
732 fn from(value: v4::Response) -> Self {
733 Self {
734 pos: value.pos,
735 txn_id: value.txn_id,
736 lists: value.lists.into_iter().map(|(room_id, list)| (room_id, list.into())).collect(),
737 rooms: value.rooms.into_iter().map(|(room_id, room)| (room_id, room.into())).collect(),
738 extensions: value.extensions.into(),
739 }
740 }
741}
742
743#[cfg(feature = "unstable-msc3575")]
744impl From<v4::SyncList> for response::List {
745 fn from(value: v4::SyncList) -> Self {
746 Self { count: value.count }
747 }
748}
749
750#[cfg(feature = "unstable-msc3575")]
751impl From<v4::SlidingSyncRoom> for response::Room {
752 fn from(value: v4::SlidingSyncRoom) -> Self {
753 Self {
754 name: value.name,
755 avatar: value.avatar,
756 initial: value.initial,
757 is_dm: value.is_dm,
758 invite_state: value.invite_state,
759 unread_notifications: value.unread_notifications,
760 timeline: value.timeline,
761 required_state: value.required_state,
762 prev_batch: value.prev_batch,
763 limited: value.limited,
764 joined_count: value.joined_count,
765 invited_count: value.invited_count,
766 num_live: value.num_live,
767 bump_stamp: value.timestamp.map(|t| t.0),
768 heroes: value.heroes.map(|heroes| heroes.into_iter().map(Into::into).collect()),
769 }
770 }
771}
772
773#[cfg(feature = "unstable-msc3575")]
774impl From<v4::SlidingSyncRoomHero> for response::Hero {
775 fn from(value: v4::SlidingSyncRoomHero) -> Self {
776 Self { user_id: value.user_id, name: value.name, avatar: value.avatar }
777 }
778}
779
780#[cfg(feature = "unstable-msc3575")]
781impl From<v4::Extensions> for response::Extensions {
782 fn from(value: v4::Extensions) -> Self {
783 Self {
784 to_device: value.to_device.map(Into::into),
785 e2ee: value.e2ee.into(),
786 account_data: value.account_data.into(),
787 receipts: value.receipts.into(),
788 typing: value.typing.into(),
789 }
790 }
791}
792
793#[cfg(feature = "unstable-msc3575")]
794impl From<v4::ToDevice> for response::ToDevice {
795 fn from(value: v4::ToDevice) -> Self {
796 Self { next_batch: value.next_batch, events: value.events }
797 }
798}
799
800#[cfg(feature = "unstable-msc3575")]
801impl From<v4::E2EE> for response::E2EE {
802 fn from(value: v4::E2EE) -> Self {
803 Self {
804 device_lists: value.device_lists,
805 device_one_time_keys_count: value.device_one_time_keys_count,
806 device_unused_fallback_key_types: value.device_unused_fallback_key_types,
807 }
808 }
809}
810
811#[cfg(feature = "unstable-msc3575")]
812impl From<v4::AccountData> for response::AccountData {
813 fn from(value: v4::AccountData) -> Self {
814 Self { global: value.global, rooms: value.rooms }
815 }
816}
817
818#[cfg(feature = "unstable-msc3575")]
819impl From<v4::Receipts> for response::Receipts {
820 fn from(value: v4::Receipts) -> Self {
821 Self { rooms: value.rooms }
822 }
823}
824
825#[cfg(feature = "unstable-msc3575")]
826impl From<v4::Typing> for response::Typing {
827 fn from(value: v4::Typing) -> Self {
828 Self { rooms: value.rooms }
829 }
830}
831
832#[cfg(test)]
833mod tests {
834 use ruma_common::owned_room_id;
835
836 use super::request::ReceiptsRoom;
837
838 #[test]
839 fn serialize_request_receipts_room() {
840 let entry = ReceiptsRoom::AllSubscribed;
841 assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""*""#);
842
843 let entry = ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz"));
844 assert_eq!(serde_json::to_string(&entry).unwrap().as_str(), r#""!foo:bar.baz""#);
845 }
846
847 #[test]
848 fn deserialize_request_receipts_room() {
849 assert_eq!(
850 serde_json::from_str::<ReceiptsRoom>(r#""*""#).unwrap(),
851 ReceiptsRoom::AllSubscribed
852 );
853
854 assert_eq!(
855 serde_json::from_str::<ReceiptsRoom>(r#""!foo:bar.baz""#).unwrap(),
856 ReceiptsRoom::Room(owned_room_id!("!foo:bar.baz"))
857 );
858 }
859}