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();
198
199 to_device_events
200 } else {
201 Vec::new()
202 };
203
204 Ok(())
205 }
206
207 pub async fn handle_room_response(
208 &mut self,
209 response: &http::Response,
210 requested_required_states: &RequestedRequiredStates,
211 state_store_guard: &MutexGuard<'_, ()>,
212 ) -> Result<()> {
213 subscribe_to_room_latest_events(&self.client, response.rooms.keys()).await;
214
215 let previously_joined_rooms = self
216 .client
217 .joined_rooms()
218 .into_iter()
219 .map(|r| r.room_id().to_owned())
220 .collect::<BTreeSet<_>>();
221
222 let mut sync_response = self
223 .client
224 .base_client()
225 .process_sliding_sync(response, requested_required_states, state_store_guard)
226 .await?;
227
228 handle_receipts_extension(&self.client, response, &mut sync_response, state_store_guard)
229 .await?;
230
231 update_in_memory_caches(&self.client, &previously_joined_rooms, &sync_response).await;
232
233 self.response = Some(sync_response);
234
235 Ok(())
236 }
237
238 pub async fn handle_thread_subscriptions(
239 &mut self,
240 previous_pos: Option<&str>,
241 thread_subs: response::ThreadSubscriptions,
242 ) -> Result<()> {
243 let catchup_token =
244 thread_subs.prev_batch.map(|prev_batch| ThreadSubscriptionCatchupToken {
245 from: prev_batch,
246 to: previous_pos.map(|s| s.to_owned()),
247 });
248
249 self.client
250 .thread_subscription_catchup()
251 .sync_subscriptions(thread_subs.subscribed, thread_subs.unsubscribed, catchup_token)
252 .await?;
253
254 Ok(())
255 }
256
257 pub async fn process_and_take_response(mut self) -> Result<SyncResponse> {
258 let mut response = self.response.take().unwrap_or_default();
259
260 response.to_device.extend(self.to_device_events);
261
262 self.client.call_sync_response_handlers(&response).await?;
263
264 Ok(response)
265 }
266}
267
268async fn update_in_memory_caches(
272 client: &Client,
273 previously_joined_rooms: &BTreeSet<OwnedRoomId>,
274 response: &SyncResponse,
275) {
276 let _timer = timer!(tracing::Level::TRACE, "update_in_memory_caches");
277
278 if response.account_data.iter().any(|event| {
281 event
282 .get_field::<GlobalAccountDataEventType>("type")
283 .ok()
284 .flatten()
285 .is_some_and(|event_type| event_type == GlobalAccountDataEventType::PushRules)
286 }) {
287 let notification_settings = client.notification_settings().await;
288 let rules = notification_settings.rules().await;
289
290 for room in client.joined_rooms() {
292 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
293 room.update_cached_user_defined_notification_mode(mode);
294 } else {
295 room.clear_user_defined_notification_mode();
296 }
297 }
298 } else {
299 let mut rules = None;
304
305 for room_id in response
306 .rooms
307 .joined
308 .keys()
309 .filter(|room_id| !previously_joined_rooms.contains(*room_id))
310 {
311 let Some(room) = client.get_room(room_id) else {
312 error!(?room_id, "The room must exist since it has been joined");
313 continue;
314 };
315
316 let rules = if let Some(rules) = &mut rules {
319 rules
320 } else {
321 rules.insert(client.notification_settings().await.rules().await.clone())
322 };
323
324 if let Some(mode) = rules.get_user_defined_room_notification_mode(room.room_id()) {
327 room.update_cached_user_defined_notification_mode(mode);
328 }
329 }
330 }
331}
332
333async fn handle_receipts_extension(
335 client: &Client,
336 response: &http::Response,
337 sync_response: &mut SyncResponse,
338 state_store_guard: &MutexGuard<'_, ()>,
339) -> Result<()> {
340 let _timer = timer!(tracing::Level::TRACE, "handle_receipts_extension");
341
342 let room_ids = BTreeSet::from_iter(
345 sync_response
346 .rooms
347 .joined
348 .keys()
349 .cloned()
350 .chain(response.extensions.receipts.rooms.keys().cloned()),
351 );
352
353 let futures = room_ids.into_iter().map(|room_id| async {
355 let receipt_event = client
356 .base_client()
357 .process_sliding_sync_receipts_extension_for_room(&room_id, response, state_store_guard)
358 .await?;
359
360 Result::<_, crate::Error>::Ok(Some((room_id, receipt_event)))
361 });
362
363 let updates = try_join_all(futures).await?;
364
365 for (room_id, receipt_event_content) in updates.into_iter().flatten() {
366 if let Some(event) = receipt_event_content {
367 sync_response.rooms.joined.entry(room_id).or_default().ephemeral.push(event.cast());
368 }
369 }
370
371 Ok(())
372}
373
374#[cfg(all(test, not(target_family = "wasm")))]
375mod tests {
376 use std::{collections::BTreeMap, ops::Not};
377
378 use assert_matches::assert_matches;
379 use matrix_sdk_base::{
380 RequestedRequiredStates, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomState,
381 notification_settings::RoomNotificationMode,
382 };
383 use matrix_sdk_test::{async_test, event_factory::EventFactory};
384 use ruma::{
385 api::client::discovery::get_supported_versions, assign, event_id, room_id, serde::Raw,
386 user_id,
387 };
388 use serde_json::json;
389 use tokio::task::yield_now;
390
391 use super::{Version, VersionBuilder};
392 use crate::{
393 SlidingSyncList, SlidingSyncMode,
394 error::Result,
395 sliding_sync::{VersionBuilderError, client::SlidingSyncResponseProcessor, http},
396 test_utils::{client::MockClientBuilder, mocks::MatrixMockServer},
397 };
398
399 #[test]
400 fn test_version_builder_none() {
401 assert_matches!(VersionBuilder::None.build(None), Ok(Version::None));
402 }
403
404 #[test]
405 fn test_version_builder_native() {
406 assert_matches!(VersionBuilder::Native.build(None), Ok(Version::Native));
407 }
408
409 #[test]
410 fn test_version_builder_discover_native() {
411 let mut response = get_supported_versions::Response::new(vec![]);
412 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), true)].into();
413
414 assert_matches!(
415 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
416 Ok(Version::Native)
417 );
418 }
419
420 #[test]
421 fn test_version_builder_discover_native_no_supported_versions() {
422 assert_matches!(
423 VersionBuilder::DiscoverNative.build(None),
424 Err(VersionBuilderError::MissingVersionsResponse)
425 );
426 }
427
428 #[test]
429 fn test_version_builder_discover_native_unstable_features_is_disabled() {
430 let mut response = get_supported_versions::Response::new(vec![]);
431 response.unstable_features = [("org.matrix.simplified_msc3575".to_owned(), false)].into();
432
433 assert_matches!(
434 VersionBuilder::DiscoverNative.build(Some(&response.as_supported_versions())),
435 Err(VersionBuilderError::NativeVersionIsUnset)
436 );
437 }
438
439 #[async_test]
440 async fn test_available_sliding_sync_versions_none() {
441 let client = MockClientBuilder::new(None).build().await;
442 let available_versions = client.available_sliding_sync_versions().await;
443
444 assert!(available_versions.is_empty());
447 }
448
449 #[async_test]
450 async fn test_available_sliding_sync_versions_native() {
451 let server = MatrixMockServer::new().await;
452 let client = server.client_builder().no_server_versions().build().await;
453
454 server.mock_versions().with_simplified_sliding_sync().ok().mock_once().mount().await;
455
456 let available_versions = client.available_sliding_sync_versions().await;
457
458 assert_eq!(available_versions.len(), 1);
460 assert_matches!(available_versions[0], Version::Native);
461 }
462
463 #[async_test]
464 async fn test_cache_user_defined_notification_mode() -> Result<()> {
465 let client = MockClientBuilder::new(None).build().await;
466 let room_id = room_id!("!r0:matrix.org");
467
468 let sliding_sync = client
469 .sliding_sync("test")?
470 .with_account_data_extension(
471 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
472 )
473 .add_list(
474 SlidingSyncList::builder("all")
475 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
476 )
477 .build()
478 .await?;
479
480 {
483 let server_response = assign!(http::Response::new("0".to_owned()), {
484 rooms: BTreeMap::from([(
485 room_id.to_owned(),
486 http::response::Room::default(),
487 )]),
488 extensions: assign!(http::response::Extensions::default(), {
489 account_data: assign!(http::response::AccountData::default(), {
490 global: vec![
491 Raw::from_json_string(
492 json!({
493 "type": "m.push_rules",
494 "content": {
495 "global": {
496 "room": [
497 {
498 "actions": ["notify"],
499 "rule_id": room_id,
500 "default": false,
501 "enabled": true,
502 },
503 ],
504 },
505 },
506 })
507 .to_string(),
508 ).unwrap()
509 ]
510 })
511 })
512 });
513
514 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
515 sliding_sync
516 .handle_response(
517 server_response.clone(),
518 &mut pos_guard,
519 RequestedRequiredStates::default(),
520 )
521 .await?;
522 }
523
524 let room = client.get_room(room_id).unwrap();
526
527 assert_eq!(
529 room.cached_user_defined_notification_mode(),
530 Some(RoomNotificationMode::AllMessages),
531 );
532
533 {
536 let server_response = assign!(http::Response::new("0".to_owned()), {
537 rooms: BTreeMap::from([(
538 room_id.to_owned(),
539 http::response::Room::default(),
540 )]),
541 extensions: assign!(http::response::Extensions::default(), {
542 account_data: assign!(http::response::AccountData::default(), {
543 global: vec![
544 Raw::from_json_string(
545 json!({
546 "type": "m.push_rules",
547 "content": {
548 "global": {
549 "room": [
550 {
551 "actions": [],
552 "rule_id": room_id,
553 "default": false,
554 "enabled": true,
555 },
556 ],
557 },
558 },
559 })
560 .to_string(),
561 ).unwrap()
562 ]
563 })
564 })
565 });
566
567 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
568 sliding_sync
569 .handle_response(
570 server_response.clone(),
571 &mut pos_guard,
572 RequestedRequiredStates::default(),
573 )
574 .await?;
575 }
576
577 assert_eq!(
579 room.cached_user_defined_notification_mode(),
580 Some(RoomNotificationMode::MentionsAndKeywordsOnly),
581 );
582
583 {
587 let server_response = assign!(http::Response::new("0".to_owned()), {
588 extensions: assign!(http::response::Extensions::default(), {
589 account_data: assign!(http::response::AccountData::default(), {
590 global: vec![
591 Raw::from_json_string(
592 json!({
593 "type": "m.push_rules",
594 "content": {
595 "global": {
596 "room": [
597 {
598 "actions": ["notify"],
599 "rule_id": room_id,
600 "default": false,
601 "enabled": true,
602 },
603 ],
604 },
605 },
606 })
607 .to_string(),
608 ).unwrap()
609 ]
610 })
611 })
612 });
613
614 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
615 sliding_sync
616 .handle_response(
617 server_response.clone(),
618 &mut pos_guard,
619 RequestedRequiredStates::default(),
620 )
621 .await?;
622 }
623
624 assert_eq!(
626 room.cached_user_defined_notification_mode(),
627 Some(RoomNotificationMode::AllMessages),
628 );
629
630 Ok(())
631 }
632
633 #[async_test]
634 async fn test_auto_listen_to_latest_events() -> Result<()> {
635 let client = MockClientBuilder::new(None).build().await;
636 let room_id = room_id!("!r0");
637
638 client.base_client().get_or_create_room(room_id, RoomState::Joined);
640
641 client.event_cache().subscribe()?;
643
644 assert!(client.latest_events().await.is_listening_to_room(room_id).await.not());
646
647 let sliding_sync = client
649 .sliding_sync("test")?
650 .add_list(
651 SlidingSyncList::builder("all")
652 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
653 )
654 .build()
655 .await?;
656
657 {
659 let server_response = assign!(http::Response::new("0".to_owned()), {
660 rooms: BTreeMap::from([(
661 room_id.to_owned(),
662 http::response::Room::default(),
663 )]),
664 });
665
666 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
667
668 sliding_sync
669 .handle_response(
670 server_response.clone(),
671 &mut pos_guard,
672 RequestedRequiredStates::default(),
673 )
674 .await?;
675 }
676
677 assert!(client.get_room(room_id).is_some());
679
680 assert!(client.latest_events().await.is_listening_to_room(room_id).await);
682
683 Ok(())
684 }
685
686 #[async_test]
687 async fn test_read_receipt_can_trigger_a_notable_update_reason() {
688 use ruma::api::client::sync::sync_events::v5 as http;
689
690 let client = MockClientBuilder::new(None).build().await;
692 client.event_cache().subscribe().unwrap();
693
694 let mut room_info_notable_update_stream = client.room_info_notable_update_receiver();
695
696 let room_id = room_id!("!r:e.uk");
698 let room = http::response::Room::new();
699 let mut response = http::Response::new("5".to_owned());
700 response.rooms.insert(room_id.to_owned(), room);
701
702 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
703 {
704 let state_store_guard = client.base_client().state_store_lock().lock().await;
705 processor
706 .handle_room_response(
707 &response,
708 &RequestedRequiredStates::default(),
709 &state_store_guard,
710 )
711 .await
712 .expect("Failed to process sync");
713 }
714 processor.process_and_take_response().await.expect("Failed to finish processing sync");
715
716 assert_matches!(
718 room_info_notable_update_stream.recv().await,
719 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
720 assert_eq!(received_room_id, room_id);
721 assert!(!received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
722 }
723 );
724 assert_matches!(
725 room_info_notable_update_stream.recv().await,
726 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
727 assert_eq!(received_room_id, room_id);
728 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::DISPLAY_NAME), "{received_reasons:?}");
729 }
730 );
731 assert!(room_info_notable_update_stream.is_empty());
732
733 let room_id = room_id!("!r:e.uk");
736 let f = EventFactory::new().room(room_id).sender(user_id!("@u:h.uk"));
737 let events = vec![
738 f.text_msg("hi").event_id(event_id!("$3")).into_raw_sync(),
739 f.text_msg("hi").event_id(event_id!("$4")).into_raw_sync(),
740 ];
741 let room = assign!(http::response::Room::new(), {
742 timeline: events,
743 });
744 let mut response = http::Response::new("5".to_owned());
745 response.rooms.insert(room_id.to_owned(), room);
746
747 let mut processor = SlidingSyncResponseProcessor::new(client.clone());
748 {
749 let state_store_guard = client.base_client().state_store_lock().lock().await;
750 processor
751 .handle_room_response(
752 &response,
753 &RequestedRequiredStates::default(),
754 &state_store_guard,
755 )
756 .await
757 .expect("Failed to process sync");
758 }
759 processor.process_and_take_response().await.expect("Failed to finish processing sync");
760
761 assert_matches!(
765 room_info_notable_update_stream.recv().await,
766 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
767 assert_eq!(received_room_id, room_id);
768 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::NONE), "{received_reasons:?}");
769 }
770 );
771 assert_matches!(
773 room_info_notable_update_stream.recv().await,
774 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
775 assert_eq!(received_room_id, room_id);
776 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::READ_RECEIPT), "{received_reasons:?}");
777 }
778 );
779
780 assert_matches!(
783 room_info_notable_update_stream.recv().await,
784 Ok(RoomInfoNotableUpdate { room_id: received_room_id, reasons: received_reasons }) => {
785 assert_eq!(received_room_id, room_id);
786 assert!(received_reasons.contains(RoomInfoNotableUpdateReasons::LATEST_EVENT), "{received_reasons:?}");
787 }
788 );
789
790 yield_now().await;
791
792 assert!(room_info_notable_update_stream.is_empty());
794 }
795}