1use std::{
2 collections::{BTreeMap, BTreeSet},
3 time::Duration,
4};
5
6use futures_util::future::try_join_all;
7use matrix_sdk_base::{
8 RequestedRequiredStates, ThreadSubscriptionCatchupToken, sync::SyncResponse, timer,
9};
10use matrix_sdk_common::{deserialized_responses::ProcessedToDeviceEvent, timeout::timeout};
11use ruma::{
12 OwnedRoomId,
13 api::{
14 FeatureFlag, SupportedVersions,
15 client::sync::sync_events::v5::{self as http, response},
16 },
17 assign,
18 directory::RoomTypeFilter,
19 events::GlobalAccountDataEventType,
20 presence::PresenceState,
21 uint,
22};
23use tokio::sync::MutexGuard;
24use tracing::error;
25
26use super::{SlidingSync, SlidingSyncBuilder};
27use crate::{Client, Result, sync::subscribe_to_room_latest_events};
28
29const HAS_ENCRYPTED_ROOMS_CONNECTION_ID: &str = "encrypted-rooms";
32
33const HAS_ENCRYPTED_ROOMS_LIST_NAME: &str = "encrypted_rooms";
36
37#[derive(Clone, Debug)]
39pub enum Version {
40 None,
43
44 Native,
47}
48
49impl Version {
50 #[cfg(test)]
51 pub(crate) fn is_native(&self) -> bool {
52 matches!(self, Self::Native)
53 }
54}
55
56#[derive(thiserror::Error, Debug)]
58pub enum VersionBuilderError {
59 #[error("The `/versions` response is not set")]
61 MissingVersionsResponse,
62
63 #[error(
66 "`/versions` does not contain `org.matrix.simplified_msc3575` in its `unstable_features`, \
67 or it's not set to true."
68 )]
69 NativeVersionIsUnset,
70}
71
72#[derive(Clone, Debug)]
74pub enum VersionBuilder {
75 None,
77
78 Native,
80
81 DiscoverNative,
85}
86
87impl VersionBuilder {
88 pub(crate) fn needs_get_supported_versions(&self) -> bool {
89 matches!(self, Self::DiscoverNative)
90 }
91
92 pub fn build(
97 self,
98 supported: Option<&SupportedVersions>,
99 ) -> Result<Version, VersionBuilderError> {
100 Ok(match self {
101 Self::None => Version::None,
102
103 Self::Native => Version::Native,
104
105 Self::DiscoverNative => {
106 let Some(supported) = supported else {
107 return Err(VersionBuilderError::MissingVersionsResponse);
108 };
109
110 if supported.features.contains(&FeatureFlag::Msc4186) {
111 Version::Native
112 } else {
113 return Err(VersionBuilderError::NativeVersionIsUnset);
114 }
115 }
116 })
117 }
118}
119
120impl Client {
121 pub async fn available_sliding_sync_versions(&self) -> Vec<Version> {
129 let supported_versions = self.supported_versions().await.ok();
130
131 [VersionBuilder::DiscoverNative]
132 .into_iter()
133 .filter_map(|version_builder| version_builder.build(supported_versions.as_ref()).ok())
134 .collect()
135 }
136
137 pub fn sliding_sync(&self, id: impl Into<String>) -> Result<SlidingSyncBuilder> {
142 Ok(SlidingSync::builder(id.into(), self.clone())?)
143 }
144
145 pub async fn has_encrypted_rooms(&self, wait: Duration) -> Result<bool> {
155 if self.joined_rooms().iter().any(|room| {
158 if room.is_space() {
159 return false;
160 }
161
162 let state = room.encryption_state();
163 state.is_encrypted() || state.is_unknown()
164 }) {
165 return Ok(true);
166 }
167
168 if matches!(self.sliding_sync_version(), Version::None) {
169 return Err(super::Error::VersionIsMissing.into());
170 }
171
172 let list = assign!(http::request::List::default(), {
175 ranges: vec![(uint!(0), uint!(0))],
178 filters: Some(assign!(http::request::ListFilters::default(), {
179 is_encrypted: Some(true),
180 is_invite: Some(false),
181 not_room_types: vec![RoomTypeFilter::Space],
182 })),
183 });
184
185 let request = assign!(http::Request::new(), {
186 conn_id: Some(HAS_ENCRYPTED_ROOMS_CONNECTION_ID.to_owned()),
187 lists: BTreeMap::from([(HAS_ENCRYPTED_ROOMS_LIST_NAME.to_owned(), list)]),
188 set_presence: PresenceState::Offline,
190 });
191
192 let response = timeout(async { self.send(request).await }, wait)
193 .await
194 .map_err(|_| crate::Error::Timeout)??;
195
196 let Some(list) = response.lists.get(HAS_ENCRYPTED_ROOMS_LIST_NAME) else {
197 return Err(crate::Error::UnknownError(
198 "the sliding sync response is missing the encrypted rooms count".into(),
199 ));
200 };
201
202 Ok(u64::from(list.count) > 0)
203 }
204
205 #[cfg(any(test, feature = "testing"))]
211 #[tracing::instrument(skip(self, response))]
212 pub async fn process_sliding_sync_test_helper(
213 &self,
214 response: &http::Response,
215 requested_required_states: &RequestedRequiredStates,
216 ) -> Result<SyncResponse> {
217 let response = self
218 .base_client()
219 .process_sliding_sync(
220 response,
221 requested_required_states,
222 &self.base_client().state_store_lock().lock().await,
223 )
224 .await?;
225
226 tracing::debug!("done processing on base_client");
227
228 if let Some(event_cache) = self.event_cache() {
230 Box::pin(event_cache.handle_room_updates(response.rooms.clone())).await?;
231 }
232
233 self.call_sync_response_handlers(&response).await?;
235
236 Ok(response)
237 }
238}
239
240#[must_use]
246pub(crate) struct SlidingSyncResponseProcessor {
247 client: Client,
248 to_device_events: Vec<ProcessedToDeviceEvent>,
249 response: Option<SyncResponse>,
250}
251
252impl SlidingSyncResponseProcessor {
253 pub fn new(client: Client) -> Self {
254 Self { client, to_device_events: Vec::new(), response: None }
255 }
256
257 #[cfg(feature = "e2e-encryption")]
258 pub async fn handle_encryption(
259 &mut self,
260 extensions: &response::Extensions,
261 state_store_guard: &MutexGuard<'_, ()>,
262 ) -> Result<()> {
263 assert!(self.response.is_none());
266
267 self.to_device_events = if let Some(to_device_events) = self
268 .client
269 .base_client()
270 .process_sliding_sync_e2ee(
271 extensions.to_device.as_ref(),
272 &extensions.e2ee,
273 state_store_guard,
274 )
275 .await?
276 {
277 self.client.encryption().backups().maybe_trigger_backup();
280
281 to_device_events
282 } else {
283 Vec::new()
284 };
285
286 Ok(())
287 }
288
289 pub async fn handle_room_response(
290 &mut self,
291 response: &http::Response,
292 requested_required_states: &RequestedRequiredStates,
293 state_store_guard: &MutexGuard<'_, ()>,
294 ) -> Result<()> {
295 subscribe_to_room_latest_events(&self.client, response.rooms.keys()).await;
296
297 let previously_joined_rooms = self
298 .client
299 .joined_rooms()
300 .into_iter()
301 .map(|r| r.room_id().to_owned())
302 .collect::<BTreeSet<_>>();
303
304 let mut sync_response = self
305 .client
306 .base_client()
307 .process_sliding_sync(response, requested_required_states, state_store_guard)
308 .await?;
309
310 handle_receipts_extension(&self.client, response, &mut sync_response, state_store_guard)
311 .await?;
312
313 update_in_memory_caches(&self.client, &previously_joined_rooms, &sync_response).await;
314
315 self.response = Some(sync_response);
316
317 Ok(())
318 }
319
320 pub async fn handle_thread_subscriptions(
321 &mut self,
322 previous_pos: Option<&str>,
323 thread_subs: response::ThreadSubscriptions,
324 ) -> Result<()> {
325 let catchup_token =
326 thread_subs.prev_batch.map(|prev_batch| ThreadSubscriptionCatchupToken {
327 from: prev_batch,
328 to: previous_pos.map(|s| s.to_owned()),
329 });
330
331 self.client
332 .thread_subscription_catchup()
333 .sync_subscriptions(thread_subs.subscribed, thread_subs.unsubscribed, catchup_token)
334 .await?;
335
336 Ok(())
337 }
338
339 pub async fn process_and_take_response(mut self) -> Result<SyncResponse> {
340 let mut response = self.response.take().unwrap_or_default();
341
342 response.to_device.extend(self.to_device_events);
343
344 if let Some(event_cache) = self.client.event_cache() {
346 Box::pin(event_cache.handle_room_updates(response.rooms.clone())).await?;
347 }
348
349 self.client.call_sync_response_handlers(&response).await?;
351
352 Ok(response)
353 }
354}
355
356async fn update_in_memory_caches(
360 client: &Client,
361 previously_joined_rooms: &BTreeSet<OwnedRoomId>,
362 response: &SyncResponse,
363) {
364 let _timer = timer!(tracing::Level::TRACE, "update_in_memory_caches");
365
366 if response.account_data.iter().any(|event| {
369 event
370 .get_field::<GlobalAccountDataEventType>("type")
371 .ok()
372 .flatten()
373 .is_some_and(|event_type| event_type == GlobalAccountDataEventType::PushRules)
374 }) {
375 let notification_settings = client.notification_settings().await;
376 let rules = notification_settings.rules().await;
377
378 for room in client.joined_rooms() {
380 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
381 room.update_cached_user_defined_notification_mode(mode);
382 } else {
383 room.clear_user_defined_notification_mode();
384 }
385 }
386 } else {
387 let mut rules = None;
392
393 for room_id in response
394 .rooms
395 .joined
396 .keys()
397 .filter(|room_id| !previously_joined_rooms.contains(*room_id))
398 {
399 let Some(room) = client.get_room(room_id) else {
400 error!(?room_id, "The room must exist since it has been joined");
401 continue;
402 };
403
404 let rules = if let Some(rules) = &mut rules {
407 rules
408 } else {
409 rules.insert(client.notification_settings().await.rules().await.clone())
410 };
411
412 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
415 room.update_cached_user_defined_notification_mode(mode);
416 }
417 }
418 }
419}
420
421async fn handle_receipts_extension(
423 client: &Client,
424 response: &http::Response,
425 sync_response: &mut SyncResponse,
426 state_store_guard: &MutexGuard<'_, ()>,
427) -> Result<()> {
428 let _timer = timer!(tracing::Level::TRACE, "handle_receipts_extension");
429
430 let room_ids = BTreeSet::from_iter(
433 sync_response
434 .rooms
435 .joined
436 .keys()
437 .cloned()
438 .chain(response.extensions.receipts.rooms.keys().cloned()),
439 );
440
441 let futures = room_ids.into_iter().map(|room_id| async {
443 let receipt_event = client
444 .base_client()
445 .process_sliding_sync_receipts_extension_for_room(&room_id, response, state_store_guard)
446 .await?;
447
448 Result::<_, crate::Error>::Ok(Some((room_id, receipt_event)))
449 });
450
451 let updates = try_join_all(futures).await?;
452
453 for (room_id, receipt_event_content) in updates.into_iter().flatten() {
454 if let Some(event) = receipt_event_content {
455 sync_response.rooms.joined.entry(room_id).or_default().ephemeral.push(event.cast());
456 }
457 }
458
459 Ok(())
460}
461
462#[cfg(all(test, not(target_family = "wasm")))]
463mod tests {
464 use std::{collections::BTreeMap, ops::Not};
465
466 use assert_matches::assert_matches;
467 use matrix_sdk_base::{
468 RequestedRequiredStates, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
469 notification_settings::RoomNotificationMode,
470 };
471 use matrix_sdk_test::{async_test, event_factory::EventFactory};
472 use ruma::{
473 api::client::discovery::get_supported_versions, assign, event_id, room_id, serde::Raw,
474 user_id,
475 };
476 use serde_json::json;
477 use tokio::task::yield_now;
478
479 use super::{Version, VersionBuilder};
480 use crate::{
481 SlidingSyncList, SlidingSyncMode,
482 error::Result,
483 sliding_sync::{VersionBuilderError, client::SlidingSyncResponseProcessor, http},
484 test_utils::{client::MockClientBuilder, mocks::MatrixMockServer},
485 };
486
487 #[test]
488 fn test_version_builder_none() {
489 assert_matches!(VersionBuilder::None.build(None), Ok(Version::None));
490 }
491
492 #[test]
493 fn test_version_builder_native() {
494 assert_matches!(VersionBuilder::Native.build(None), Ok(Version::Native));
495 }
496
497 #[test]
498 fn test_version_builder_discover_native() {
499 let mut response = get_supported_versions::Response::new(vec![]);
500 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), true)].into();
501
502 assert_matches!(
503 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
504 Ok(Version::Native)
505 );
506 }
507
508 #[test]
509 fn test_version_builder_discover_native_no_supported_versions() {
510 assert_matches!(
511 VersionBuilder::DiscoverNative.build(None),
512 Err(VersionBuilderError::MissingVersionsResponse)
513 );
514 }
515
516 #[test]
517 fn test_version_builder_discover_native_unstable_features_is_disabled() {
518 let mut response = get_supported_versions::Response::new(vec![]);
519 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), false)].into();
520
521 assert_matches!(
522 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
523 Err(VersionBuilderError::NativeVersionIsUnset)
524 );
525 }
526
527 #[async_test]
528 async fn test_available_sliding_sync_versions_none() {
529 let client = MockClientBuilder::new(None).build().await;
530 let available_versions = client.available_sliding_sync_versions().await;
531
532 assert!(available_versions.is_empty());
535 }
536
537 #[async_test]
538 async fn test_available_sliding_sync_versions_native() {
539 let server = MatrixMockServer::new().await;
540 let client = server.client_builder().no_server_versions().build().await;
541
542 server.mock_versions().with_simplified_sliding_sync().ok().mock_once().mount().await;
543
544 let available_versions = client.available_sliding_sync_versions().await;
545
546 assert_eq!(available_versions.len(), 1);
548 assert_matches!(available_versions[0], Version::Native);
549 }
550
551 #[async_test]
552 async fn test_cache_user_defined_notification_mode() -> Result<()> {
553 let client = MockClientBuilder::new(None).build().await;
554 let room_id = room_id!("!r0:matrix.org");
555
556 let sliding_sync = client
557 .sliding_sync("test")?
558 .with_account_data_extension(
559 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
560 )
561 .add_list(
562 SlidingSyncList::builder("all")
563 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
564 )
565 .build()
566 .await?;
567
568 {
571 let server_response = assign!(http::Response::new("0".to_owned()), {
572 rooms: BTreeMap::from([(
573 room_id.to_owned(),
574 http::response::Room::default(),
575 )]),
576 extensions: assign!(http::response::Extensions::default(), {
577 account_data: assign!(http::response::AccountData::default(), {
578 global: vec![
579 Raw::from_json_string(
580 json!({
581 "type": "m.push_rules",
582 "content": {
583 "global": {
584 "room": [
585 {
586 "actions": ["notify"],
587 "rule_id": room_id,
588 "default": false,
589 "enabled": true,
590 },
591 ],
592 },
593 },
594 })
595 .to_string(),
596 ).unwrap()
597 ]
598 })
599 })
600 });
601
602 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
603 sliding_sync
604 .handle_response(
605 server_response.clone(),
606 &mut pos_guard,
607 RequestedRequiredStates::default(),
608 )
609 .await?;
610 }
611
612 let room = client.get_room(room_id).unwrap();
614
615 assert_eq!(
617 room.cached_user_defined_notification_mode(),
618 Some(RoomNotificationMode::AllMessages),
619 );
620
621 {
624 let server_response = assign!(http::Response::new("0".to_owned()), {
625 rooms: BTreeMap::from([(
626 room_id.to_owned(),
627 http::response::Room::default(),
628 )]),
629 extensions: assign!(http::response::Extensions::default(), {
630 account_data: assign!(http::response::AccountData::default(), {
631 global: vec![
632 Raw::from_json_string(
633 json!({
634 "type": "m.push_rules",
635 "content": {
636 "global": {
637 "room": [
638 {
639 "actions": [],
640 "rule_id": room_id,
641 "default": false,
642 "enabled": true,
643 },
644 ],
645 },
646 },
647 })
648 .to_string(),
649 ).unwrap()
650 ]
651 })
652 })
653 });
654
655 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
656 sliding_sync
657 .handle_response(
658 server_response.clone(),
659 &mut pos_guard,
660 RequestedRequiredStates::default(),
661 )
662 .await?;
663 }
664
665 assert_eq!(
667 room.cached_user_defined_notification_mode(),
668 Some(RoomNotificationMode::MentionsAndKeywordsOnly),
669 );
670
671 {
675 let server_response = assign!(http::Response::new("0".to_owned()), {
676 extensions: assign!(http::response::Extensions::default(), {
677 account_data: assign!(http::response::AccountData::default(), {
678 global: vec![
679 Raw::from_json_string(
680 json!({
681 "type": "m.push_rules",
682 "content": {
683 "global": {
684 "room": [
685 {
686 "actions": ["notify"],
687 "rule_id": room_id,
688 "default": false,
689 "enabled": true,
690 },
691 ],
692 },
693 },
694 })
695 .to_string(),
696 ).unwrap()
697 ]
698 })
699 })
700 });
701
702 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
703 sliding_sync
704 .handle_response(
705 server_response.clone(),
706 &mut pos_guard,
707 RequestedRequiredStates::default(),
708 )
709 .await?;
710 }
711
712 assert_eq!(
714 room.cached_user_defined_notification_mode(),
715 Some(RoomNotificationMode::AllMessages),
716 );
717
718 Ok(())
719 }
720
721 #[async_test]
722 async fn test_auto_listen_to_latest_events() -> Result<()> {
723 let client = MockClientBuilder::new(None).build().await;
724 let room_id = room_id!("!r0");
725
726 client.base_client().get_or_create_room(room_id, RoomState::Joined);
728
729 assert!(client.latest_events().await.unwrap().is_listening_to_room(room_id).await.not());
731
732 let sliding_sync = client
734 .sliding_sync("test")?
735 .add_list(
736 SlidingSyncList::builder("all")
737 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
738 )
739 .build()
740 .await?;
741
742 {
744 let server_response = assign!(http::Response::new("0".to_owned()), {
745 rooms: BTreeMap::from([(
746 room_id.to_owned(),
747 http::response::Room::default(),
748 )]),
749 });
750
751 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
752
753 sliding_sync
754 .handle_response(
755 server_response.clone(),
756 &mut pos_guard,
757 RequestedRequiredStates::default(),
758 )
759 .await?;
760 }
761
762 assert!(client.get_room(room_id).is_some());
764
765 assert!(client.latest_events().await.unwrap().is_listening_to_room(room_id).await);
767
768 Ok(())
769 }
770
771 #[async_test]
772 async fn test_read_receipt_can_trigger_a_notable_update_reason() {
773 use ruma::api::client::sync::sync_events::v5 as http;
774
775 let client = MockClientBuilder::new(None).build().await;
777
778 let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
779
780 let room_id = room_id!("!r:e.uk");
782 let room = http::response::Room::new();
783 let mut response = http::Response::new("5".to_owned());
784 response.rooms.insert(room_id.to_owned(), room);
785
786 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
787 {
788 let state_store_guard = client.base_client().state_store_lock().lock().await;
789 processor
790 .handle_room_response(
791 &response,
792 &RequestedRequiredStates::default(),
793 &state_store_guard,
794 )
795 .await
796 .expect("Failed to process sync");
797 }
798 processor.process_and_take_response().await.expect("Failed to finish processing sync");
799
800 assert_matches!(
802 room_info_notable_update_stream.recv().await,
803 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
804 assert_eq!(received_room_id, room_id);
805 assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
806 }
807 );
808 assert_matches!(
809 room_info_notable_update_stream.recv().await,
810 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
811 assert_eq!(received_room_id, room_id);
812 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
813 }
814 );
815 assert!(room_info_notable_update_stream.is_empty());
816
817 let room_id = room_id!("!r:e.uk");
820 let f = EventFactory::new().room(room_id).sender(user_id!("@u:h.uk"));
821 let events = vec![
822 f.text_msg("hi").event_id(event_id!("$3")).into_raw_sync(),
823 f.text_msg("hi").event_id(event_id!("$4")).into_raw_sync(),
824 ];
825 let room = assign!(http::response::Room::new(), {
826 timeline: events,
827 });
828 let mut response = http::Response::new("5".to_owned());
829 response.rooms.insert(room_id.to_owned(), room);
830
831 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
832 {
833 let state_store_guard = client.base_client().state_store_lock().lock().await;
834 processor
835 .handle_room_response(
836 &response,
837 &RequestedRequiredStates::default(),
838 &state_store_guard,
839 )
840 .await
841 .expect("Failed to process sync");
842 }
843 processor.process_and_take_response().await.expect("Failed to finish processing sync");
844
845 assert_matches!(
849 room_info_notable_update_stream.recv().await,
850 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
851 assert_eq!(received_room_id, room_id);
852 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
853 }
854 );
855 assert_matches!(
857 room_info_notable_update_stream.recv().await,
858 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
859 assert_eq!(received_room_id, room_id);
860 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
861 }
862 );
863
864 assert_matches!(
867 room_info_notable_update_stream.recv().await,
868 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
869 assert_eq!(received_room_id, room_id);
870 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT), "{received_reasons:?}");
871 }
872 );
873
874 yield_now().await;
875
876 assert!(room_info_notable_update_stream.is_empty());
878 }
879}