1use std::collections::BTreeSet;
2
3use futures_util::future::try_join_all;
4use matrix_sdk_base::{
5 RequestedRequiredStates, ThreadSubscriptionCatchupToken, sync::SyncResponse, timer,
6};
7use matrix_sdk_common::deserialized_responses::ProcessedToDeviceEvent;
8use ruma::{
9 OwnedRoomId,
10 api::{
11 FeatureFlag, SupportedVersions,
12 client::sync::sync_events::v5::{self as http, response},
13 },
14 events::GlobalAccountDataEventType,
15};
16use tokio::sync::MutexGuard;
17use tracing::error;
18
19use super::{SlidingSync, SlidingSyncBuilder};
20use crate::{Client, Result, sync::subscribe_to_room_latest_events};
21
22#[derive(Clone, Debug)]
24pub enum Version {
25 None,
28
29 Native,
32}
33
34impl Version {
35 #[cfg(test)]
36 pub(crate) fn is_native(&self) -> bool {
37 matches!(self, Self::Native)
38 }
39}
40
41#[derive(thiserror::Error, Debug)]
43pub enum VersionBuilderError {
44 #[error("The `/versions` response is not set")]
46 MissingVersionsResponse,
47
48 #[error(
51 "`/versions` does not contain `org.matrix.simplified_msc3575` in its `unstable_features`, \
52 or it's not set to true."
53 )]
54 NativeVersionIsUnset,
55}
56
57#[derive(Clone, Debug)]
59pub enum VersionBuilder {
60 None,
62
63 Native,
65
66 DiscoverNative,
70}
71
72impl VersionBuilder {
73 pub(crate) fn needs_get_supported_versions(&self) -> bool {
74 matches!(self, Self::DiscoverNative)
75 }
76
77 pub fn build(
82 self,
83 supported: Option<&SupportedVersions>,
84 ) -> Result<Version, VersionBuilderError> {
85 Ok(match self {
86 Self::None => Version::None,
87
88 Self::Native => Version::Native,
89
90 Self::DiscoverNative => {
91 let Some(supported) = supported else {
92 return Err(VersionBuilderError::MissingVersionsResponse);
93 };
94
95 if supported.features.contains(&FeatureFlag::Msc4186) {
96 Version::Native
97 } else {
98 return Err(VersionBuilderError::NativeVersionIsUnset);
99 }
100 }
101 })
102 }
103}
104
105impl Client {
106 pub async fn available_sliding_sync_versions(&self) -> Vec<Version> {
114 let supported_versions = self.supported_versions().await.ok();
115
116 [VersionBuilder::DiscoverNative]
117 .into_iter()
118 .filter_map(|version_builder| version_builder.build(supported_versions.as_ref()).ok())
119 .collect()
120 }
121
122 pub fn sliding_sync(&self, id: impl Into<String>) -> Result<SlidingSyncBuilder> {
127 Ok(SlidingSync::builder(id.into(), self.clone())?)
128 }
129
130 #[cfg(any(test, feature = "testing"))]
136 #[tracing::instrument(skip(self, response))]
137 pub async fn process_sliding_sync_test_helper(
138 &self,
139 response: &http::Response,
140 requested_required_states: &RequestedRequiredStates,
141 ) -> Result<SyncResponse> {
142 let response = self
143 .base_client()
144 .process_sliding_sync(
145 response,
146 requested_required_states,
147 &self.base_client().state_store_lock().lock().await,
148 )
149 .await?;
150
151 tracing::debug!("done processing on base_client");
152 self.call_sync_response_handlers(&response).await?;
153
154 Ok(response)
155 }
156}
157
158#[must_use]
164pub(crate) struct SlidingSyncResponseProcessor {
165 client: Client,
166 to_device_events: Vec<ProcessedToDeviceEvent>,
167 response: Option<SyncResponse>,
168}
169
170impl SlidingSyncResponseProcessor {
171 pub fn new(client: Client) -> Self {
172 Self { client, to_device_events: Vec::new(), response: None }
173 }
174
175 #[cfg(feature = "e2e-encryption")]
176 pub async fn handle_encryption(
177 &mut self,
178 extensions: &response::Extensions,
179 state_store_guard: &MutexGuard<'_, ()>,
180 ) -> Result<()> {
181 assert!(self.response.is_none());
184
185 self.to_device_events = if let Some(to_device_events) = self
186 .client
187 .base_client()
188 .process_sliding_sync_e2ee(
189 extensions.to_device.as_ref(),
190 &extensions.e2ee,
191 state_store_guard,
192 )
193 .await?
194 {
195 self.client.encryption().backups().maybe_trigger_backup();
197
198 to_device_events
199 } else {
200 Vec::new()
201 };
202
203 Ok(())
204 }
205
206 pub async fn handle_room_response(
207 &mut self,
208 response: &http::Response,
209 requested_required_states: &RequestedRequiredStates,
210 state_store_guard: &MutexGuard<'_, ()>,
211 ) -> Result<()> {
212 subscribe_to_room_latest_events(&self.client, response.rooms.keys()).await;
213
214 let previously_joined_rooms = self
215 .client
216 .joined_rooms()
217 .into_iter()
218 .map(|r| r.room_id().to_owned())
219 .collect::<BTreeSet<_>>();
220
221 let mut sync_response = self
222 .client
223 .base_client()
224 .process_sliding_sync(response, requested_required_states, state_store_guard)
225 .await?;
226
227 handle_receipts_extension(&self.client, response, &mut sync_response, state_store_guard)
228 .await?;
229
230 update_in_memory_caches(&self.client, &previously_joined_rooms, &sync_response).await;
231
232 self.response = Some(sync_response);
233
234 Ok(())
235 }
236
237 pub async fn handle_thread_subscriptions(
238 &mut self,
239 previous_pos: Option<&str>,
240 thread_subs: response::ThreadSubscriptions,
241 ) -> Result<()> {
242 let catchup_token =
243 thread_subs.prev_batch.map(|prev_batch| ThreadSubscriptionCatchupToken {
244 from: prev_batch,
245 to: previous_pos.map(|s| s.to_owned()),
246 });
247
248 self.client
249 .thread_subscription_catchup()
250 .sync_subscriptions(thread_subs.subscribed, thread_subs.unsubscribed, catchup_token)
251 .await?;
252
253 Ok(())
254 }
255
256 pub async fn process_and_take_response(mut self) -> Result<SyncResponse> {
257 let mut response = self.response.take().unwrap_or_default();
258
259 response.to_device.extend(self.to_device_events);
260
261 self.client.call_sync_response_handlers(&response).await?;
262
263 Ok(response)
264 }
265}
266
267async fn update_in_memory_caches(
271 client: &Client,
272 previously_joined_rooms: &BTreeSet<OwnedRoomId>,
273 response: &SyncResponse,
274) {
275 let _timer = timer!(tracing::Level::TRACE, "update_in_memory_caches");
276
277 if response.account_data.iter().any(|event| {
280 event
281 .get_field::<GlobalAccountDataEventType>("type")
282 .ok()
283 .flatten()
284 .is_some_and(|event_type| event_type == GlobalAccountDataEventType::PushRules)
285 }) {
286 let notification_settings = client.notification_settings().await;
287 let rules = notification_settings.rules().await;
288
289 for room in client.joined_rooms() {
291 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
292 room.update_cached_user_defined_notification_mode(mode);
293 } else {
294 room.clear_user_defined_notification_mode();
295 }
296 }
297 } else {
298 let mut rules = None;
303
304 for room_id in response
305 .rooms
306 .joined
307 .keys()
308 .filter(|room_id| !previously_joined_rooms.contains(*room_id))
309 {
310 let Some(room) = client.get_room(room_id) else {
311 error!(?room_id, "The room must exist since it has been joined");
312 continue;
313 };
314
315 let rules = if let Some(rules) = &mut rules {
317 rules
318 } else {
319 rules.insert(client.notification_settings().await.rules().await.clone())
320 };
321
322 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
324 room.update_cached_user_defined_notification_mode(mode);
325 }
326 }
327 }
328}
329
330async fn handle_receipts_extension(
332 client: &Client,
333 response: &http::Response,
334 sync_response: &mut SyncResponse,
335 state_store_guard: &MutexGuard<'_, ()>,
336) -> Result<()> {
337 let _timer = timer!(tracing::Level::TRACE, "handle_receipts_extension");
338
339 let room_ids = BTreeSet::from_iter(
342 sync_response
343 .rooms
344 .joined
345 .keys()
346 .cloned()
347 .chain(response.extensions.receipts.rooms.keys().cloned()),
348 );
349
350 let futures = room_ids.into_iter().map(|room_id| async {
352 let receipt_event = client
353 .base_client()
354 .process_sliding_sync_receipts_extension_for_room(&room_id, response, state_store_guard)
355 .await?;
356
357 Result::<_, crate::Error>::Ok(Some((room_id, receipt_event)))
358 });
359
360 let updates = try_join_all(futures).await?;
361
362 for (room_id, receipt_event_content) in updates.into_iter().flatten() {
363 if let Some(event) = receipt_event_content {
364 sync_response.rooms.joined.entry(room_id).or_default().ephemeral.push(event.cast());
365 }
366 }
367
368 Ok(())
369}
370
371#[cfg(all(test, not(target_family = "wasm")))]
372mod tests {
373 use std::{collections::BTreeMap, ops::Not};
374
375 use assert_matches::assert_matches;
376 use matrix_sdk_base::{
377 RequestedRequiredStates, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
378 notification_settings::RoomNotificationMode,
379 };
380 use matrix_sdk_test::{async_test, event_factory::EventFactory};
381 use ruma::{
382 api::client::discovery::get_supported_versions, assign, event_id, room_id, serde::Raw,
383 user_id,
384 };
385 use serde_json::json;
386 use tokio::task::yield_now;
387
388 use super::{Version, VersionBuilder};
389 use crate::{
390 SlidingSyncList, SlidingSyncMode,
391 error::Result,
392 sliding_sync::{VersionBuilderError, client::SlidingSyncResponseProcessor, http},
393 test_utils::{client::MockClientBuilder, mocks::MatrixMockServer},
394 };
395
396 #[test]
397 fn test_version_builder_none() {
398 assert_matches!(VersionBuilder::None.build(None), Ok(Version::None));
399 }
400
401 #[test]
402 fn test_version_builder_native() {
403 assert_matches!(VersionBuilder::Native.build(None), Ok(Version::Native));
404 }
405
406 #[test]
407 fn test_version_builder_discover_native() {
408 let mut response = get_supported_versions::Response::new(vec![]);
409 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), true)].into();
410
411 assert_matches!(
412 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
413 Ok(Version::Native)
414 );
415 }
416
417 #[test]
418 fn test_version_builder_discover_native_no_supported_versions() {
419 assert_matches!(
420 VersionBuilder::DiscoverNative.build(None),
421 Err(VersionBuilderError::MissingVersionsResponse)
422 );
423 }
424
425 #[test]
426 fn test_version_builder_discover_native_unstable_features_is_disabled() {
427 let mut response = get_supported_versions::Response::new(vec![]);
428 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), false)].into();
429
430 assert_matches!(
431 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
432 Err(VersionBuilderError::NativeVersionIsUnset)
433 );
434 }
435
436 #[async_test]
437 async fn test_available_sliding_sync_versions_none() {
438 let client = MockClientBuilder::new(None).build().await;
439 let available_versions = client.available_sliding_sync_versions().await;
440
441 assert!(available_versions.is_empty());
444 }
445
446 #[async_test]
447 async fn test_available_sliding_sync_versions_native() {
448 let server = MatrixMockServer::new().await;
449 let client = server.client_builder().no_server_versions().build().await;
450
451 server.mock_versions().with_simplified_sliding_sync().ok().mock_once().mount().await;
452
453 let available_versions = client.available_sliding_sync_versions().await;
454
455 assert_eq!(available_versions.len(), 1);
457 assert_matches!(available_versions[0], Version::Native);
458 }
459
460 #[async_test]
461 async fn test_cache_user_defined_notification_mode() -> Result<()> {
462 let client = MockClientBuilder::new(None).build().await;
463 let room_id = room_id!("!r0:matrix.org");
464
465 let sliding_sync = client
466 .sliding_sync("test")?
467 .with_account_data_extension(
468 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
469 )
470 .add_list(
471 SlidingSyncList::builder("all")
472 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
473 )
474 .build()
475 .await?;
476
477 {
480 let server_response = assign!(http::Response::new("0".to_owned()), {
481 rooms: BTreeMap::from([(
482 room_id.to_owned(),
483 http::response::Room::default(),
484 )]),
485 extensions: assign!(http::response::Extensions::default(), {
486 account_data: assign!(http::response::AccountData::default(), {
487 global: vec![
488 Raw::from_json_string(
489 json!({
490 "type": "m.push_rules",
491 "content": {
492 "global": {
493 "room": [
494 {
495 "actions": ["notify"],
496 "rule_id": room_id,
497 "default": false,
498 "enabled": true,
499 },
500 ],
501 },
502 },
503 })
504 .to_string(),
505 ).unwrap()
506 ]
507 })
508 })
509 });
510
511 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
512 sliding_sync
513 .handle_response(
514 server_response.clone(),
515 &mut pos_guard,
516 RequestedRequiredStates::default(),
517 )
518 .await?;
519 }
520
521 let room = client.get_room(room_id).unwrap();
523
524 assert_eq!(
526 room.cached_user_defined_notification_mode(),
527 Some(RoomNotificationMode::AllMessages),
528 );
529
530 {
534 let server_response = assign!(http::Response::new("0".to_owned()), {
535 rooms: BTreeMap::from([(
536 room_id.to_owned(),
537 http::response::Room::default(),
538 )]),
539 extensions: assign!(http::response::Extensions::default(), {
540 account_data: assign!(http::response::AccountData::default(), {
541 global: vec![
542 Raw::from_json_string(
543 json!({
544 "type": "m.push_rules",
545 "content": {
546 "global": {
547 "room": [
548 {
549 "actions": [],
550 "rule_id": room_id,
551 "default": false,
552 "enabled": true,
553 },
554 ],
555 },
556 },
557 })
558 .to_string(),
559 ).unwrap()
560 ]
561 })
562 })
563 });
564
565 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
566 sliding_sync
567 .handle_response(
568 server_response.clone(),
569 &mut pos_guard,
570 RequestedRequiredStates::default(),
571 )
572 .await?;
573 }
574
575 assert_eq!(
577 room.cached_user_defined_notification_mode(),
578 Some(RoomNotificationMode::MentionsAndKeywordsOnly),
579 );
580
581 {
585 let server_response = assign!(http::Response::new("0".to_owned()), {
586 extensions: assign!(http::response::Extensions::default(), {
587 account_data: assign!(http::response::AccountData::default(), {
588 global: vec![
589 Raw::from_json_string(
590 json!({
591 "type": "m.push_rules",
592 "content": {
593 "global": {
594 "room": [
595 {
596 "actions": ["notify"],
597 "rule_id": room_id,
598 "default": false,
599 "enabled": true,
600 },
601 ],
602 },
603 },
604 })
605 .to_string(),
606 ).unwrap()
607 ]
608 })
609 })
610 });
611
612 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
613 sliding_sync
614 .handle_response(
615 server_response.clone(),
616 &mut pos_guard,
617 RequestedRequiredStates::default(),
618 )
619 .await?;
620 }
621
622 assert_eq!(
624 room.cached_user_defined_notification_mode(),
625 Some(RoomNotificationMode::AllMessages),
626 );
627
628 Ok(())
629 }
630
631 #[async_test]
632 async fn test_auto_listen_to_latest_events() -> Result<()> {
633 let client = MockClientBuilder::new(None).build().await;
634 let room_id = room_id!("!r0");
635
636 client.base_client().get_or_create_room(room_id, RoomState::Joined);
638
639 client.event_cache().subscribe()?;
641
642 assert!(client.latest_events().await.is_listening_to_room(room_id).await.not());
644
645 let sliding_sync = client
647 .sliding_sync("test")?
648 .add_list(
649 SlidingSyncList::builder("all")
650 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
651 )
652 .build()
653 .await?;
654
655 {
657 let server_response = assign!(http::Response::new("0".to_owned()), {
658 rooms: BTreeMap::from([(
659 room_id.to_owned(),
660 http::response::Room::default(),
661 )]),
662 });
663
664 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
665
666 sliding_sync
667 .handle_response(
668 server_response.clone(),
669 &mut pos_guard,
670 RequestedRequiredStates::default(),
671 )
672 .await?;
673 }
674
675 assert!(client.get_room(room_id).is_some());
677
678 assert!(client.latest_events().await.is_listening_to_room(room_id).await);
680
681 Ok(())
682 }
683
684 #[async_test]
685 async fn test_read_receipt_can_trigger_a_notable_update_reason() {
686 use ruma::api::client::sync::sync_events::v5 as http;
687
688 let client = MockClientBuilder::new(None).build().await;
690 client.event_cache().subscribe().unwrap();
691
692 let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
693
694 let room_id = room_id!("!r:e.uk");
696 let room = http::response::Room::new();
697 let mut response = http::Response::new("5".to_owned());
698 response.rooms.insert(room_id.to_owned(), room);
699
700 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
701 {
702 let state_store_guard = client.base_client().state_store_lock().lock().await;
703 processor
704 .handle_room_response(
705 &response,
706 &RequestedRequiredStates::default(),
707 &state_store_guard,
708 )
709 .await
710 .expect("Failed to process sync");
711 }
712 processor.process_and_take_response().await.expect("Failed to finish processing sync");
713
714 assert_matches!(
716 room_info_notable_update_stream.recv().await,
717 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
718 assert_eq!(received_room_id, room_id);
719 assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
720 }
721 );
722 assert_matches!(
723 room_info_notable_update_stream.recv().await,
724 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
725 assert_eq!(received_room_id, room_id);
726 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
727 }
728 );
729 assert!(room_info_notable_update_stream.is_empty());
730
731 let room_id = room_id!("!r:e.uk");
734 let f = EventFactory::new().room(room_id).sender(user_id!("@u:h.uk"));
735 let events = vec![
736 f.text_msg("hi").event_id(event_id!("$3")).into_raw_sync(),
737 f.text_msg("hi").event_id(event_id!("$4")).into_raw_sync(),
738 ];
739 let room = assign!(http::response::Room::new(), {
740 timeline: events,
741 });
742 let mut response = http::Response::new("5".to_owned());
743 response.rooms.insert(room_id.to_owned(), room);
744
745 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
746 {
747 let state_store_guard = client.base_client().state_store_lock().lock().await;
748 processor
749 .handle_room_response(
750 &response,
751 &RequestedRequiredStates::default(),
752 &state_store_guard,
753 )
754 .await
755 .expect("Failed to process sync");
756 }
757 processor.process_and_take_response().await.expect("Failed to finish processing sync");
758
759 assert_matches!(
763 room_info_notable_update_stream.recv().await,
764 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
765 assert_eq!(received_room_id, room_id);
766 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
767 }
768 );
769 assert_matches!(
771 room_info_notable_update_stream.recv().await,
772 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
773 assert_eq!(received_room_id, room_id);
774 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
775 }
776 );
777
778 assert_matches!(
781 room_info_notable_update_stream.recv().await,
782 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
783 assert_eq!(received_room_id, room_id);
784 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT), "{received_reasons:?}");
785 }
786 );
787
788 yield_now().await;
789
790 assert!(room_info_notable_update_stream.is_empty());
792 }
793}