1#![doc = include_str!("README.md")]
17
18mod builder;
19mod cache;
20mod client;
21mod error;
22mod list;
23
24use std::{
25 collections::{BTreeMap, btree_map::Entry},
26 fmt::Debug,
27 future::Future,
28 sync::{Arc, RwLock as StdRwLock},
29 time::Duration,
30};
31
32use async_stream::stream;
33pub use client::{Version, VersionBuilder};
34use futures_core::stream::Stream;
35use matrix_sdk_base::RequestedRequiredStates;
36#[cfg(feature = "e2e-encryption")]
37use matrix_sdk_common::executor::JoinHandleExt as _;
38use matrix_sdk_common::{executor::spawn, timer};
39#[cfg(feature = "e2e-encryption")]
40use ruma::api::error::{ErrorBody, StandardErrorBody};
41use ruma::{
42 OwnedRoomId, RoomId,
43 api::{client::sync::sync_events::v5 as http, error::ErrorKind},
44 assign,
45};
46use tokio::{
47 select,
48 sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender},
49};
50use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn};
51
52pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
53use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
54use crate::{Client, Result, config::RequestConfig};
55
56#[derive(Clone, Debug)]
60pub struct SlidingSync {
61 inner: Arc<SlidingSyncInner>,
63}
64
65#[derive(Debug)]
66pub(super) struct SlidingSyncInner {
67 id: String,
71
72 client: Client,
74
75 poll_timeout: Duration,
77
78 network_timeout: Duration,
81
82 storage_key: String,
84
85 share_pos: bool,
92
93 position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
106
107 lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
109
110 room_subscriptions: StdRwLock<BTreeMap<OwnedRoomId, http::request::RoomSubscription>>,
113
114 extensions: http::request::Extensions,
117
118 internal_channel: Sender<SlidingSyncInternalMessage>,
121}
122
123impl SlidingSync {
124 pub(super) fn new(inner: SlidingSyncInner) -> Self {
125 Self { inner: Arc::new(inner) }
126 }
127
128 async fn cache_to_storage(&self, position: &SlidingSyncPositionMarkers) -> Result<()> {
129 cache::store_sliding_sync_state(self, position).await
130 }
131
132 pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
134 SlidingSyncBuilder::new(id, client)
135 }
136
137 pub fn add_room_subscriptions(
147 &self,
148 room_ids: &[&RoomId],
149 settings: Option<http::request::RoomSubscription>,
150 cancel_in_flight_request: bool,
151 ) {
152 let subscriptions_have_changed = upsert_room_subscriptions(
153 &mut self.inner.room_subscriptions.write().unwrap(),
154 &self.inner.client,
155 room_ids,
156 settings,
157 );
158
159 if cancel_in_flight_request && subscriptions_have_changed {
160 self.inner.cancel_in_flight_request();
161 }
162 }
163
164 pub fn remove_room_subscriptions(&self, room_ids: &[&RoomId], cancel_in_flight_request: bool) {
166 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
167 let mut subscriptions_have_changed = false;
168
169 for room_id in room_ids {
170 if room_subscriptions.remove(*room_id).is_some() {
171 subscriptions_have_changed = true;
172 }
173 }
174
175 if cancel_in_flight_request && subscriptions_have_changed {
176 self.inner.cancel_in_flight_request();
177 }
178 }
179
180 pub fn set_room_subscriptions(
197 &self,
198 room_ids: &[&RoomId],
199 settings: Option<http::request::RoomSubscription>,
200 cancel_in_flight_request: bool,
201 ) {
202 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
203
204 let number_of_subscriptions_before = room_subscriptions.len();
207 room_subscriptions.retain(|room_id, _| room_ids.contains(&room_id.as_ref()));
208 let a_subscription_has_been_removed =
209 room_subscriptions.len() != number_of_subscriptions_before;
210
211 let a_subscription_has_been_added_or_updated = upsert_room_subscriptions(
214 &mut room_subscriptions,
215 &self.inner.client,
216 room_ids,
217 settings,
218 );
219
220 if cancel_in_flight_request
223 && (a_subscription_has_been_added_or_updated || a_subscription_has_been_removed)
224 {
225 self.inner.cancel_in_flight_request();
226 }
227 }
228
229 pub fn reset_and_add_room_subscriptions(
235 &self,
236 room_ids: &[&RoomId],
237 settings: Option<http::request::RoomSubscription>,
238 cancel_in_flight_request: bool,
239 ) {
240 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
241
242 let a_subscription_has_been_removed = !room_subscriptions.is_empty();
243 room_subscriptions.clear();
244
245 let a_subscription_has_been_added = upsert_room_subscriptions(
246 &mut room_subscriptions,
247 &self.inner.client,
248 room_ids,
249 settings,
250 );
251
252 if cancel_in_flight_request
253 && (a_subscription_has_been_added || a_subscription_has_been_removed)
254 {
255 self.inner.cancel_in_flight_request();
256 }
257 }
258
259 pub async fn on_list<Function, FunctionOutput, R>(
261 &self,
262 list_name: &str,
263 function: Function,
264 ) -> Option<R>
265 where
266 Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
267 FunctionOutput: Future<Output = R>,
268 {
269 let lists = self.inner.lists.read().await;
270
271 match lists.get(list_name) {
272 Some(list) => Some(function(list).await),
273 None => None,
274 }
275 }
276
277 pub async fn add_list(
282 &self,
283 list_builder: SlidingSyncListBuilder,
284 ) -> Result<Option<SlidingSyncList>> {
285 let list = list_builder.build(self.inner.internal_channel.clone());
286
287 let old_list = self.inner.lists.write().await.insert(list.name().to_owned(), list);
288
289 self.inner.internal_channel_send_if_possible(
290 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
291 );
292
293 Ok(old_list)
294 }
295
296 pub async fn add_cached_list(
303 &self,
304 mut list_builder: SlidingSyncListBuilder,
305 ) -> Result<Option<SlidingSyncList>> {
306 let _timer = timer!(format!("restoring (loading+processing) list {}", list_builder.name));
307
308 list_builder.set_cached_and_reload(&self.inner.client, &self.inner.storage_key).await?;
309
310 self.add_list(list_builder).await
311 }
312
313 #[instrument(skip_all)]
315 async fn handle_response(
316 &self,
317 mut sliding_sync_response: http::Response,
318 position: &mut SlidingSyncPositionMarkers,
319 requested_required_states: RequestedRequiredStates,
320 ) -> Result<UpdateSummary, crate::Error> {
321 let pos = Some(sliding_sync_response.pos.clone());
322
323 let must_process_rooms_response = self.must_process_rooms_response().await;
324
325 trace!(yes = must_process_rooms_response, "Must process rooms response?");
326
327 let sync_response = {
335 let _timer = timer!("response processor");
336
337 let response_processor = {
338 let state_store_guard = {
342 let _timer = timer!("acquiring the `state_store_lock`");
343
344 self.inner.client.base_client().state_store_lock().lock().await
345 };
346
347 let mut response_processor =
348 SlidingSyncResponseProcessor::new(self.inner.client.clone());
349
350 if self.is_thread_subscriptions_enabled() {
357 response_processor
358 .handle_thread_subscriptions(
359 position.pos.as_deref(),
360 std::mem::take(
361 &mut sliding_sync_response.extensions.thread_subscriptions,
362 ),
363 )
364 .await?;
365 }
366
367 #[cfg(feature = "e2e-encryption")]
368 if self.is_e2ee_enabled() {
369 response_processor
370 .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
371 .await?
372 }
373
374 if must_process_rooms_response {
377 response_processor
378 .handle_room_response(
379 &sliding_sync_response,
380 &requested_required_states,
381 &state_store_guard,
382 )
383 .await?;
384 }
385
386 response_processor
387 };
388
389 response_processor.process_and_take_response().await?
391 };
392
393 debug!("Sliding Sync response has been handled by the client");
394 trace!(?sync_response);
395
396 let update_summary = {
397 let updated_rooms = {
399 let mut updated_rooms = Vec::with_capacity(
400 sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
401 );
402
403 updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
404
405 updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
415
416 updated_rooms
417 };
418
419 let updated_lists = {
421 debug!(
422 lists = ?sliding_sync_response.lists,
423 "Update lists"
424 );
425
426 let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
427 let mut lists = self.inner.lists.write().await;
428
429 for (name, list) in lists.iter_mut() {
433 if let Some(updates) = sliding_sync_response.lists.get(name) {
434 let maximum_number_of_rooms: u32 =
435 updates.count.try_into().expect("failed to convert `count` to `u32`");
436
437 if list.update(Some(maximum_number_of_rooms))? {
438 updated_lists.push(name.clone());
439 }
440 } else if list.update(None)? {
441 updated_lists.push(name.clone());
442 }
443 }
444
445 for name in sliding_sync_response.lists.keys() {
447 if !lists.contains_key(name) {
448 error!("Response for list `{name}` - unknown to us; skipping");
449 }
450 }
451
452 updated_lists
453 };
454
455 UpdateSummary { lists: updated_lists, rooms: updated_rooms }
456 };
457
458 debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
462
463 position.pos = pos;
464
465 #[cfg(feature = "unstable-msc4354")]
468 if let Some(next_batch) = sliding_sync_response.extensions.sticky_events.next_batch.take() {
469 position.sticky_events_since = Some(next_batch);
470 }
471
472 Ok(update_summary)
473 }
474
475 async fn generate_sync_request(
476 &self,
477 ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
478 let mut requests_lists = BTreeMap::new();
480
481 let timeout = {
482 let lists = self.inner.lists.read().await;
483
484 let mut timeout = PollTimeout::Default;
486
487 for (name, list) in lists.iter() {
488 requests_lists.insert(name.clone(), list.next_request()?);
489 timeout = timeout.min(list.requires_timeout());
490 }
491
492 timeout
493 };
494
495 let mut position_guard = {
504 debug!("Waiting to acquire the `position` lock");
505
506 let _timer = timer!("acquiring the `position` lock");
507
508 self.inner.position.clone().lock_owned().await
509 };
510
511 debug!(pos = ?position_guard.pos, "Got a position");
512
513 let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
514
515 let restored_fields = if self.inner.share_pos || to_device_enabled {
516 restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
517 } else {
518 None
519 };
520
521 let pos = if self.inner.share_pos {
524 if let Some(fields) = &restored_fields {
525 if fields.pos != position_guard.pos {
528 info!(
529 "Pos from previous request ('{:?}') was different from \
530 pos in database ('{:?}').",
531 position_guard.pos, fields.pos
532 );
533 position_guard.pos = fields.pos.clone();
534 }
535 fields.pos.clone()
536 } else {
537 position_guard.pos.clone()
538 }
539 } else {
540 position_guard.pos.clone()
541 };
542
543 #[cfg(feature = "e2e-encryption")]
552 if pos.is_none() && self.is_e2ee_enabled() {
553 info!("Marking all tracked users as dirty");
554
555 let olm_machine = self.inner.client.olm_machine().await;
556 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
557 olm_machine.mark_all_tracked_users_as_dirty().await?;
558 }
559
560 let timeout = match timeout {
565 PollTimeout::None => None,
566 PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
567 PollTimeout::Default => Some(self.inner.poll_timeout),
568 };
569
570 Span::current()
571 .record("pos", &pos)
572 .record("timeout", timeout.map(|duration| duration.as_millis()));
573
574 let mut request = assign!(http::Request::new(), {
575 conn_id: Some(self.inner.id.clone()),
576 pos,
577 set_presence: self.inner.client.sync_presence(),
578 timeout,
579 lists: requests_lists,
580 });
581
582 request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
584
585 request.extensions = self.inner.extensions.clone();
587
588 if to_device_enabled {
590 request.extensions.to_device.since =
591 restored_fields.and_then(|fields| fields.to_device_token);
592 }
593
594 #[cfg(feature = "unstable-msc4354")]
596 if self.is_sticky_events_enabled() {
597 request.extensions.sticky_events.since = position_guard.sticky_events_since.clone();
598 }
599
600 Ok((
601 request,
603 RequestConfig::default()
606 .timeout(self.inner.poll_timeout + self.inner.network_timeout)
607 .retry_limit(3),
608 position_guard,
609 ))
610 }
611
612 async fn send_sync_request(
616 &self,
617 request: http::Request,
618 request_config: RequestConfig,
619 mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
620 ) -> Result<UpdateSummary> {
621 debug!("Sending request");
622
623 let requested_required_states = RequestedRequiredStates::from(&request);
625 let request = self.inner.client.send(request).with_request_config(request_config);
626
627 #[cfg(feature = "e2e-encryption")]
635 let response = {
636 if self.is_e2ee_enabled() {
637 let client = self.inner.client.clone();
656 let e2ee_uploads = spawn(
657 async move {
658 if let Err(error) = client.send_outgoing_requests().await {
659 error!(?error, "Error while sending outgoing E2EE requests");
660 }
661 }
662 .instrument(Span::current()),
663 )
664 .abort_on_drop();
667
668 let response = request.await?;
670
671 e2ee_uploads.await.map_err(|error| Error::JoinError {
676 task_description: "e2ee_uploads".to_owned(),
677 error,
678 })?;
679
680 response
681 } else {
682 request.await?
683 }
684 };
685
686 #[cfg(not(feature = "e2e-encryption"))]
689 let response = request.await?;
690
691 debug!("Received response");
692
693 let this = self.clone();
704
705 let future = async move {
708 debug!("Start handling response");
709
710 let updates = this
716 .handle_response(response, &mut position_guard, requested_required_states)
717 .await?;
718
719 this.cache_to_storage(&position_guard).await?;
720
721 drop(position_guard);
724
725 debug!("Done handling response");
726
727 Ok(updates)
728 };
729
730 spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
731 task_description: "handle_response".to_owned(),
732 error,
733 })?
734 }
735
736 #[cfg(feature = "e2e-encryption")]
738 fn is_e2ee_enabled(&self) -> bool {
739 self.inner.extensions.e2ee.enabled == Some(true)
740 }
741
742 fn is_thread_subscriptions_enabled(&self) -> bool {
745 self.inner.extensions.thread_subscriptions.enabled == Some(true)
746 }
747
748 #[cfg(feature = "unstable-msc4354")]
750 fn is_sticky_events_enabled(&self) -> bool {
751 self.inner.extensions.sticky_events.enabled == Some(true)
752 }
753
754 #[cfg(not(feature = "e2e-encryption"))]
755 fn is_e2ee_enabled(&self) -> bool {
756 false
757 }
758
759 async fn must_process_rooms_response(&self) -> bool {
761 !self.inner.room_subscriptions.read().unwrap().is_empty()
764 || !self.inner.lists.read().await.is_empty()
765 }
766
767 #[doc(hidden)]
771 #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
772 pub async fn sync_once(&self) -> Result<UpdateSummary> {
773 let (request, request_config, position_guard) = self.generate_sync_request().await?;
774
775 let summaries = self.send_sync_request(request, request_config, position_guard).await?;
777
778 self.inner.client.inner.sync_beat.notify(usize::MAX);
780
781 Ok(summaries)
782 }
783
784 #[allow(unknown_lints, clippy::let_with_type_underscore)] #[instrument(name = "sync_stream", skip_all, fields(conn_id = self.inner.id, with_e2ee = self.is_e2ee_enabled()))]
794 pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
795 debug!("Starting sync stream");
796
797 let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
798
799 stream! {
800 loop {
801 debug!("Sync stream is running");
802
803 select! {
804 biased;
805
806 internal_message = internal_channel_receiver.recv() => {
807 use SlidingSyncInternalMessage::*;
808
809 debug!(?internal_message, "Sync stream has received an internal message");
810
811 match internal_message {
812 Err(_) | Ok(SyncLoopStop) => {
813 break;
814 }
815
816 Ok(SyncLoopSkipOverCurrentIteration) => {
817 continue;
818 }
819 }
820 }
821
822 update_summary = self.sync_once() => {
823 match update_summary {
824 Ok(updates) => {
825 yield Ok(updates);
826 }
827
828 Err(error) => {
830 if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
831 self.expire_session().await;
833 }
834
835 #[cfg(feature = "e2e-encryption")]
836 if is_synapse_incompatible_token_error(&error)
837 && let Some(olm_machine) = &*self.inner.client.olm_machine().await {
838 if let Ok(()) = olm_machine.store().delete_next_batch_token().await {
843 warn!("Outdated to_device token loaded from cache. Retrying without token.");
844 continue;
845 }
846 }
847
848 yield Err(error);
849
850 break;
852 }
853 }
854 }
855 }
856 }
857
858 debug!("Sync stream has exited.");
859 }
860 }
861
862 pub fn stop_sync(&self) -> Result<()> {
871 Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
872 }
873
874 #[doc(hidden)]
885 pub async fn expire_session(&self) {
886 info!("Session expired; resetting `pos`");
887
888 {
889 let lists = self.inner.lists.read().await;
890
891 for list in lists.values() {
892 list.set_maximum_number_of_rooms(None);
894 }
895 }
896
897 {
899 let mut position = self.inner.position.lock().await;
900
901 position.pos = None;
903
904 #[cfg(feature = "unstable-msc4354")]
907 {
908 position.sticky_events_since = None;
909 }
910
911 if let Err(err) = self.cache_to_storage(&position).await {
914 warn!("Failed to invalidate cached sliding sync state: {err}");
915 }
916 }
917
918 {
919 self.inner.room_subscriptions.write().unwrap().clear();
922 }
923 }
924}
925
926#[cfg(feature = "e2e-encryption")]
932fn is_synapse_incompatible_token_error(error: &crate::Error) -> bool {
933 let Some(ruma::api::error::Error {
934 status_code: ::http::StatusCode::BAD_REQUEST,
935 body: ErrorBody::Standard(StandardErrorBody { message, .. }),
936 ..
937 }) = error.as_client_api_error()
938 else {
939 return false;
940 };
941
942 message.starts_with("1 validation error for SlidingSyncBody")
943 && message.contains("'extensions.to_device.since' is invalid")
944 && message.contains("(should look like an int)")
945}
946
947fn upsert_room_subscriptions(
956 room_subscriptions: &mut BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
957 client: &Client,
958 room_ids: &[&RoomId],
959 settings: Option<http::request::RoomSubscription>,
960) -> bool {
961 let settings = settings.unwrap_or_default();
962 let mut subscriptions_have_changed = false;
963
964 for room_id in room_ids {
965 match room_subscriptions.entry((*room_id).to_owned()) {
966 Entry::Vacant(entry) => {
967 if let Some(room) = client.get_room(room_id) {
968 room.mark_members_missing();
969 }
970
971 entry.insert(settings.clone());
972
973 subscriptions_have_changed = true;
974 }
975
976 Entry::Occupied(mut entry) => {
979 if room_subscriptions_differ(entry.get(), &settings) {
980 entry.insert(settings.clone());
981
982 subscriptions_have_changed = true;
983 }
984 }
985 }
986 }
987
988 subscriptions_have_changed
989}
990
991fn room_subscriptions_differ(
993 left: &http::request::RoomSubscription,
994 right: &http::request::RoomSubscription,
995) -> bool {
996 left.timeline_limit != right.timeline_limit || left.required_state != right.required_state
997}
998
999impl SlidingSyncInner {
1000 #[instrument]
1002 fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
1003 self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
1004 }
1005
1006 #[instrument]
1009 fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
1010 let _ = self.internal_channel.send(message);
1012 }
1013
1014 fn cancel_in_flight_request(&self) {
1017 self.internal_channel_send_if_possible(
1018 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
1019 );
1020 }
1021}
1022
1023#[derive(Copy, Clone, Debug, PartialEq)]
1024enum SlidingSyncInternalMessage {
1025 SyncLoopStop,
1027
1028 SyncLoopSkipOverCurrentIteration,
1031}
1032
1033#[cfg(any(test, feature = "testing"))]
1034impl SlidingSync {
1035 pub async fn set_pos(&self, new_pos: String) {
1037 let mut position_lock = self.inner.position.lock().await;
1038 position_lock.pos = Some(new_pos);
1039 }
1040}
1041
1042#[derive(Clone, Debug)]
1043pub(super) struct SlidingSyncPositionMarkers {
1044 pos: Option<String>,
1047
1048 #[cfg(feature = "unstable-msc4354")]
1056 sticky_events_since: Option<String>,
1057}
1058
1059#[derive(Debug, Clone)]
1062pub struct UpdateSummary {
1063 pub lists: Vec<String>,
1065 pub rooms: Vec<OwnedRoomId>,
1067}
1068
1069#[derive(Debug)]
1078pub enum PollTimeout {
1079 None,
1081
1082 Some(u32),
1085
1086 Default,
1089}
1090
1091impl PollTimeout {
1092 fn min(self, left: Self) -> Self {
1103 match (self, left) {
1104 (Self::None, _) => Self::None,
1105
1106 (Self::Some(_), Self::None) => Self::None,
1107 (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
1108 (Self::Some(right), Self::Default) => Self::Some(right),
1109
1110 (Self::Default, Self::None) => Self::None,
1111 (Self::Default, Self::Some(left)) => Self::Some(left),
1112 (Self::Default, Self::Default) => Self::Default,
1113 }
1114 }
1115}
1116
1117#[cfg(all(test, not(target_family = "wasm")))]
1118#[allow(clippy::dbg_macro)]
1119mod tests {
1120 use std::{
1121 collections::BTreeMap,
1122 future::ready,
1123 ops::Not,
1124 pin::pin,
1125 sync::{Arc, Mutex},
1126 time::Duration,
1127 };
1128
1129 use assert_matches::assert_matches;
1130 use event_listener::Listener;
1131 use futures_util::{StreamExt, future::join_all};
1132 use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1133 use matrix_sdk_common::executor::spawn;
1134 use matrix_sdk_test::{ALICE, User, async_test, event_factory::EventFactory};
1135 use ruma::{
1136 OwnedRoomId, assign,
1137 events::{direct::DirectEvent, room::member::MembershipState},
1138 owned_room_id,
1139 presence::PresenceState,
1140 profile::{
1141 AvatarUrl, DisplayName, ProfileFieldName, UserProfileChanges, UserProfileUpdate,
1142 },
1143 room_id,
1144 serde::Raw,
1145 uint,
1146 };
1147 use serde::Deserialize;
1148 use serde_json::json;
1149 use stream_assert::assert_pending;
1150 use wiremock::{
1151 Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1152 };
1153
1154 use super::{
1155 SlidingSync, SlidingSyncBuilder, SlidingSyncInternalMessage, SlidingSyncList,
1156 SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http,
1157 };
1158 use crate::{
1159 Client, Result,
1160 test_utils::{logged_in_client, mocks::MatrixMockServer},
1161 };
1162
1163 #[derive(Copy, Clone)]
1164 struct SlidingSyncMatcher;
1165
1166 impl Match for SlidingSyncMatcher {
1167 fn matches(&self, request: &Request) -> bool {
1168 request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1169 && request.method == Method::POST
1170 }
1171 }
1172
1173 async fn new_sliding_sync(
1174 lists: Vec<SlidingSyncListBuilder>,
1175 ) -> Result<(MockServer, SlidingSync)> {
1176 let server = MockServer::start().await;
1177 let client = logged_in_client(Some(server.uri())).await;
1178
1179 let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1180
1181 for list in lists {
1182 sliding_sync_builder = sliding_sync_builder.add_list(list);
1183 }
1184
1185 let sliding_sync = sliding_sync_builder.build().await?;
1186
1187 Ok((server, sliding_sync))
1188 }
1189
1190 #[async_test]
1191 async fn test_subscribe_to_own_profile() {
1192 let client = logged_in_client(None).await;
1193 let own_user_id = client.user_id().expect("client should be logged in").to_owned();
1194
1195 let mut response = http::Response::new("0".to_owned());
1198
1199 let mut profile_changes = UserProfileChanges::new();
1200 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Example"));
1201
1202 response
1203 .extensions
1204 .profiles
1205 .users
1206 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1207 client
1208 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1209 .await
1210 .expect("Failed to process sync");
1211
1212 let stream = client.subscribe_to_own_profile().expect("client should be logged in");
1214 let mut stream = pin!(stream);
1215
1216 let profile = stream.next().await.expect("should emit the initial profile");
1217 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1218
1219 let mut response = http::Response::new("1".to_owned());
1221
1222 let mut profile_changes = UserProfileChanges::new();
1223 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Alice"));
1224 response
1225 .extensions
1226 .profiles
1227 .users
1228 .insert(ALICE.to_owned(), UserProfileUpdate::Updated(profile_changes));
1229 client
1230 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1231 .await
1232 .expect("Failed to process sync");
1233
1234 assert_pending!(stream);
1235
1236 let mut response = http::Response::new("2".to_owned());
1238
1239 let mut profile_changes = UserProfileChanges::new();
1240 profile_changes
1241 .updated
1242 .insert(ProfileFieldName::AvatarUrl, json!("mxc://example.org/avatar"));
1243
1244 response
1245 .extensions
1246 .profiles
1247 .users
1248 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1249 client
1250 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1251 .await
1252 .expect("Failed to process sync");
1253
1254 let profile = stream.next().await.expect("should emit the updated profile");
1255 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1256 assert_eq!(
1257 profile.get_static::<AvatarUrl>().unwrap().map(|url| url.to_string()).as_deref(),
1258 Some("mxc://example.org/avatar")
1259 );
1260 }
1261
1262 #[async_test]
1263 async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1264 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1265 let client = sliding_sync.inner.client.clone();
1266
1267 {
1268 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1269
1270 assert_eq!(request.set_presence, PresenceState::Online);
1271 }
1272
1273 client.set_presence(PresenceState::Unavailable, None, false).await?;
1274
1275 {
1276 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1277
1278 assert_eq!(request.set_presence, PresenceState::Unavailable);
1279 }
1280
1281 client.set_presence(PresenceState::Offline, None, false).await?;
1282
1283 {
1284 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1285
1286 assert_eq!(request.set_presence, PresenceState::Offline);
1287 }
1288
1289 Ok(())
1290 }
1291
1292 #[async_test]
1293 async fn test_add_room_subscriptions() -> Result<()> {
1294 let (server, sliding_sync) = new_sliding_sync(vec![
1295 SlidingSyncList::builder("foo")
1296 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1297 ])
1298 .await?;
1299
1300 let stream = sliding_sync.sync();
1301 let mut stream = pin!(stream);
1302
1303 let room_id_0 = room_id!("!r0:bar.org");
1304 let room_id_1 = room_id!("!r1:bar.org");
1305 let room_id_2 = room_id!("!r2:bar.org");
1306
1307 {
1308 let _mock_guard = Mock::given(SlidingSyncMatcher)
1309 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1310 "pos": "1",
1311 "lists": {},
1312 "rooms": {
1313 room_id_0: {
1314 "name": "Room #0",
1315 "initial": true,
1316 },
1317 room_id_1: {
1318 "name": "Room #1",
1319 "initial": true,
1320 },
1321 room_id_2: {
1322 "name": "Room #2",
1323 "initial": true,
1324 },
1325 }
1326 })))
1327 .mount_as_scoped(&server)
1328 .await;
1329
1330 let _ = stream.next().await.unwrap()?;
1331 }
1332
1333 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1334
1335 assert!(room0.are_members_synced().not());
1339
1340 {
1341 struct MemberMatcher(OwnedRoomId);
1342
1343 impl Match for MemberMatcher {
1344 fn matches(&self, request: &Request) -> bool {
1345 request.url.path()
1346 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1347 && request.method == Method::GET
1348 }
1349 }
1350
1351 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1352 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1353 "chunk": [],
1354 })))
1355 .mount_as_scoped(&server)
1356 .await;
1357
1358 assert_matches!(room0.request_members().await, Ok(()));
1359 }
1360
1361 assert!(room0.are_members_synced());
1363
1364 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, true);
1365
1366 assert!(room0.are_members_synced().not());
1369
1370 {
1371 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1372
1373 assert!(room_subscriptions.contains_key(room_id_0));
1374 assert!(room_subscriptions.contains_key(room_id_1));
1375 assert!(!room_subscriptions.contains_key(room_id_2));
1376 }
1377
1378 {
1381 struct MemberMatcher(OwnedRoomId);
1382
1383 impl Match for MemberMatcher {
1384 fn matches(&self, request: &Request) -> bool {
1385 request.url.path()
1386 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1387 && request.method == Method::GET
1388 }
1389 }
1390
1391 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1392 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1393 "chunk": [],
1394 })))
1395 .mount_as_scoped(&server)
1396 .await;
1397
1398 assert_matches!(room0.request_members().await, Ok(()));
1399 }
1400
1401 assert!(room0.are_members_synced());
1403
1404 sliding_sync.add_room_subscriptions(&[room_id_0], None, false);
1405
1406 assert!(room0.are_members_synced());
1409
1410 Ok(())
1411 }
1412
1413 #[async_test]
1414 async fn test_add_remove_and_reset_room_subscriptions() -> Result<()> {
1415 let (_server, sliding_sync) = new_sliding_sync(vec![
1416 SlidingSyncList::builder("foo")
1417 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1418 ])
1419 .await?;
1420
1421 let room_id_0 = room_id!("!r0:bar.org");
1422 let room_id_1 = room_id!("!r1:bar.org");
1423 let room_id_2 = room_id!("!r2:bar.org");
1424 let room_id_3 = room_id!("!r3:bar.org");
1425
1426 {
1428 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1429
1430 assert!(room_subscriptions.is_empty());
1431 }
1432
1433 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1435
1436 {
1437 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1438
1439 assert_eq!(room_subscriptions.len(), 2);
1440 assert!(room_subscriptions.contains_key(room_id_0));
1441 assert!(room_subscriptions.contains_key(room_id_1));
1442 }
1443
1444 sliding_sync.remove_room_subscriptions(&[room_id_0], false);
1446
1447 {
1448 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1449
1450 assert_eq!(room_subscriptions.len(), 1);
1451 assert!(room_subscriptions.contains_key(room_id_1));
1452 }
1453
1454 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1456
1457 {
1458 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1459
1460 assert_eq!(room_subscriptions.len(), 2);
1461 assert!(room_subscriptions.contains_key(room_id_0));
1462 assert!(room_subscriptions.contains_key(room_id_1));
1463 }
1464
1465 sliding_sync.reset_and_add_room_subscriptions(
1467 &[room_id_2, room_id_3],
1468 Default::default(),
1469 false,
1470 );
1471
1472 {
1473 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1474
1475 assert_eq!(room_subscriptions.len(), 2);
1476 assert!(room_subscriptions.contains_key(room_id_2));
1477 assert!(room_subscriptions.contains_key(room_id_3));
1478 }
1479
1480 Ok(())
1481 }
1482
1483 #[async_test]
1484 async fn test_set_room_subscriptions_refreshes_the_settings() -> Result<()> {
1485 let (_server, sliding_sync) = new_sliding_sync(vec![
1486 SlidingSyncList::builder("foo")
1487 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1488 ])
1489 .await?;
1490
1491 let room_id_0 = room_id!("!r0:bar.org");
1492
1493 let settings = |timeline_limit: u32| {
1494 Some(assign!(http::request::RoomSubscription::default(), {
1495 timeline_limit: timeline_limit.into(),
1496 }))
1497 };
1498 let timeline_limit_of_room_0 = || {
1499 sliding_sync
1500 .inner
1501 .room_subscriptions
1502 .read()
1503 .unwrap()
1504 .get(room_id_0)
1505 .map(|subscription| subscription.timeline_limit)
1506 };
1507
1508 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1509
1510 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1512
1513 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1514 assert_matches!(
1515 internal_channel.try_recv(),
1516 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1517 );
1518
1519 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1521
1522 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1523 assert!(internal_channel.try_recv().is_err());
1524
1525 sliding_sync.set_room_subscriptions(&[room_id_0], settings(42), true);
1528
1529 assert_eq!(timeline_limit_of_room_0(), Some(42u32.into()));
1530 assert_matches!(
1531 internal_channel.try_recv(),
1532 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1533 );
1534
1535 Ok(())
1536 }
1537
1538 #[async_test]
1539 async fn test_set_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1540 let (_server, sliding_sync) = new_sliding_sync(vec![
1541 SlidingSyncList::builder("foo")
1542 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1543 ])
1544 .await?;
1545
1546 let room_id_0 = room_id!("!r0:bar.org");
1547 let room_id_1 = room_id!("!r1:bar.org");
1548
1549 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1550
1551 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1554
1555 assert_matches!(
1556 internal_channel.try_recv(),
1557 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1558 );
1559
1560 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1563
1564 assert!(internal_channel.try_recv().is_err());
1565
1566 sliding_sync.set_room_subscriptions(&[room_id_1], None, true);
1568
1569 assert_matches!(
1570 internal_channel.try_recv(),
1571 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1572 );
1573
1574 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1576
1577 assert!(internal_channel.try_recv().is_err());
1578
1579 Ok(())
1580 }
1581
1582 #[async_test]
1583 async fn test_reset_and_add_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1584 let (_server, sliding_sync) = new_sliding_sync(vec![
1585 SlidingSyncList::builder("foo")
1586 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1587 ])
1588 .await?;
1589
1590 let room_id_0 = room_id!("!r0:bar.org");
1591
1592 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1593
1594 sliding_sync.reset_and_add_room_subscriptions(&[], None, true);
1596
1597 assert!(internal_channel.try_recv().is_err());
1598
1599 sliding_sync.reset_and_add_room_subscriptions(&[room_id_0], None, true);
1601
1602 assert_matches!(
1603 internal_channel.try_recv(),
1604 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1605 );
1606
1607 sliding_sync.reset_and_add_room_subscriptions(&[], None, true);
1610
1611 assert!(sliding_sync.inner.room_subscriptions.read().unwrap().is_empty());
1612 assert_matches!(
1613 internal_channel.try_recv(),
1614 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1615 );
1616
1617 sliding_sync.reset_and_add_room_subscriptions(&[room_id_0], None, false);
1619
1620 assert!(internal_channel.try_recv().is_err());
1621
1622 Ok(())
1623 }
1624
1625 #[async_test]
1626 async fn test_set_room_subscriptions() -> Result<()> {
1627 let (server, sliding_sync) = new_sliding_sync(vec![
1628 SlidingSyncList::builder("foo")
1629 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1630 ])
1631 .await?;
1632
1633 let stream = sliding_sync.sync();
1634 let mut stream = pin!(stream);
1635
1636 let room_id_0 = room_id!("!r0:bar.org");
1637 let room_id_1 = room_id!("!r1:bar.org");
1638 let room_id_2 = room_id!("!r2:bar.org");
1639
1640 {
1641 let _mock_guard = Mock::given(SlidingSyncMatcher)
1642 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1643 "pos": "1",
1644 "lists": {},
1645 "rooms": {
1646 room_id_0: {
1647 "name": "Room #0",
1648 "initial": true,
1649 },
1650 room_id_1: {
1651 "name": "Room #1",
1652 "initial": true,
1653 },
1654 room_id_2: {
1655 "name": "Room #2",
1656 "initial": true,
1657 },
1658 }
1659 })))
1660 .mount_as_scoped(&server)
1661 .await;
1662
1663 let _ = stream.next().await.unwrap()?;
1664 }
1665
1666 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1667
1668 assert!(room0.are_members_synced().not());
1672
1673 {
1674 struct MemberMatcher(OwnedRoomId);
1675
1676 impl Match for MemberMatcher {
1677 fn matches(&self, request: &Request) -> bool {
1678 request.url.path()
1679 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1680 && request.method == Method::GET
1681 }
1682 }
1683
1684 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1685 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1686 "chunk": [],
1687 })))
1688 .mount_as_scoped(&server)
1689 .await;
1690
1691 assert_matches!(room0.request_members().await, Ok(()));
1692 }
1693
1694 assert!(room0.are_members_synced());
1696
1697 sliding_sync.set_room_subscriptions(&[room_id_0, room_id_1], None, true);
1698
1699 assert!(room0.are_members_synced().not());
1702
1703 {
1705 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1706
1707 assert!(room_subscriptions.contains_key(room_id_0));
1708 assert!(room_subscriptions.contains_key(room_id_1));
1709 assert!(!room_subscriptions.contains_key(room_id_2));
1710 }
1711
1712 {
1715 struct MemberMatcher(OwnedRoomId);
1716
1717 impl Match for MemberMatcher {
1718 fn matches(&self, request: &Request) -> bool {
1719 request.url.path()
1720 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1721 && request.method == Method::GET
1722 }
1723 }
1724
1725 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1726 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1727 "chunk": [],
1728 })))
1729 .mount_as_scoped(&server)
1730 .await;
1731
1732 assert_matches!(room0.request_members().await, Ok(()));
1733 }
1734
1735 assert!(room0.are_members_synced());
1737
1738 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1739
1740 assert!(room0.are_members_synced());
1743
1744 {
1746 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1747
1748 assert!(room_subscriptions.contains_key(room_id_0));
1749 assert!(!room_subscriptions.contains_key(room_id_1));
1750 assert!(!room_subscriptions.contains_key(room_id_2));
1751 }
1752 Ok(())
1753 }
1754
1755 #[async_test]
1756 async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1757 let (_server, sliding_sync) = new_sliding_sync(vec![
1758 SlidingSyncList::builder("foo")
1759 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1760 ])
1761 .await?;
1762
1763 let room_id_0 = room_id!("!r0:bar.org");
1764 let room_id_1 = room_id!("!r1:bar.org");
1765 let room_id_2 = room_id!("!r2:bar.org");
1766
1767 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, false);
1769
1770 {
1771 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1772
1773 assert!(room_subscriptions.contains_key(room_id_0));
1774 assert!(room_subscriptions.contains_key(room_id_1));
1775 assert!(room_subscriptions.contains_key(room_id_2).not());
1776 }
1777
1778 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1780
1781 {
1782 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1783
1784 assert!(room_subscriptions.contains_key(room_id_0));
1785 assert!(room_subscriptions.contains_key(room_id_1));
1786 assert!(room_subscriptions.contains_key(room_id_2));
1787 }
1788
1789 sliding_sync.expire_session().await;
1791
1792 {
1793 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1794
1795 assert!(room_subscriptions.is_empty());
1796 }
1797
1798 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1800
1801 {
1802 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1803
1804 assert!(room_subscriptions.contains_key(room_id_0).not());
1805 assert!(room_subscriptions.contains_key(room_id_1).not());
1806 assert!(room_subscriptions.contains_key(room_id_2));
1807 }
1808
1809 Ok(())
1810 }
1811
1812 #[async_test]
1813 async fn test_add_list() -> Result<()> {
1814 let (_server, sliding_sync) = new_sliding_sync(vec![
1815 SlidingSyncList::builder("foo")
1816 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1817 ])
1818 .await?;
1819
1820 let _stream = sliding_sync.sync();
1821 let mut _stream = pin!(_stream);
1822
1823 sliding_sync
1824 .add_list(
1825 SlidingSyncList::builder("bar")
1826 .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1827 )
1828 .await?;
1829
1830 let lists = sliding_sync.inner.lists.read().await;
1831
1832 assert!(lists.contains_key("foo"));
1833 assert!(lists.contains_key("bar"));
1834
1835 Ok(())
1839 }
1840
1841 #[cfg(feature = "e2e-encryption")]
1842 #[async_test]
1843 async fn test_extensions_to_device_since_is_set() {
1844 use matrix_sdk_base::crypto::store::types::Changes;
1845
1846 let client = logged_in_client(None).await;
1847 let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1848 .unwrap()
1849 .with_to_device_extension(assign!(
1850 http::request::ToDevice::default(),
1851 {
1852 enabled: Some(true),
1853 }
1854 ))
1855 .build()
1856 .await
1857 .unwrap();
1858
1859 {
1861 let to_device = &sliding_sync.inner.extensions.to_device;
1862
1863 assert_eq!(to_device.enabled, Some(true));
1864 assert!(to_device.since.is_none());
1865 }
1866
1867 {
1869 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1870
1871 let to_device = &request.extensions.to_device;
1872
1873 assert_eq!(to_device.enabled, Some(true));
1874 assert!(to_device.since.is_none());
1875 }
1876
1877 let since_token = "depuis".to_owned();
1879
1880 {
1881 if let Some(olm_machine) = &*client.olm_machine().await {
1882 olm_machine
1883 .store()
1884 .save_changes(Changes {
1885 next_batch_token: Some(since_token.clone()),
1886 ..Default::default()
1887 })
1888 .await
1889 .unwrap();
1890 } else {
1891 panic!("Where is the Olm machine?");
1892 }
1893 }
1894
1895 {
1897 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1898
1899 let to_device = &request.extensions.to_device;
1900
1901 assert_eq!(to_device.enabled, Some(true));
1902 assert_eq!(to_device.since, Some(since_token));
1903 }
1904 }
1905
1906 #[cfg(feature = "unstable-msc4354")]
1907 #[async_test]
1908 async fn test_extensions_sticky_events_since_is_set() -> Result<()> {
1909 let server = MockServer::start().await;
1910
1911 #[derive(Deserialize)]
1912 struct PartialRequest {
1913 txn_id: Option<String>,
1914 }
1915
1916 let response_count = Arc::new(Mutex::new(0));
1919 let _mock_guard = Mock::given(SlidingSyncMatcher)
1920 .respond_with(move |request: &Request| {
1921 let request: PartialRequest = request.body_json().unwrap();
1922 let count = {
1923 let mut count = response_count.lock().unwrap();
1924 *count += 1;
1925 *count
1926 };
1927
1928 let mut response = json!({
1929 "txn_id": request.txn_id,
1930 "pos": count.to_string(),
1931 });
1932
1933 if count <= 2 {
1934 response["extensions"] = json!({
1935 "org.matrix.msc4354.sticky_events": {
1936 "next_batch": format!("sticky_{count}"),
1937 }
1938 });
1939 }
1940
1941 ResponseTemplate::new(200).set_body_json(response)
1942 })
1943 .mount_as_scoped(&server)
1944 .await;
1945
1946 let client = logged_in_client(Some(server.uri())).await;
1947
1948 let sliding_sync = client
1949 .sliding_sync("sticky")?
1950 .with_sticky_events_extension(assign!(
1951 http::request::StickyEvents::default(),
1952 { enabled: Some(true) }
1953 ))
1954 .build()
1955 .await?;
1956
1957 {
1959 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1960 assert_eq!(request.extensions.sticky_events.enabled, Some(true));
1961 assert!(request.extensions.sticky_events.since.is_none());
1962 }
1963
1964 sliding_sync.sync_once().await?;
1966
1967 {
1968 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1969 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_1"));
1970 }
1971
1972 sliding_sync.sync_once().await?;
1973
1974 {
1975 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1976 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_2"));
1977 }
1978
1979 sliding_sync.sync_once().await?;
1981
1982 {
1983 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1984 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_2"));
1985 }
1986
1987 sliding_sync.expire_session().await;
1989
1990 {
1991 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1992 assert!(request.pos.is_none());
1993 assert!(request.extensions.sticky_events.since.is_none());
1994 }
1995
1996 Ok(())
1997 }
1998
1999 #[async_test]
2005 #[cfg(feature = "e2e-encryption")]
2006 async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
2007 use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
2008 use matrix_sdk_test::ruma_response_from_json;
2009 use ruma::user_id;
2010
2011 let server = MockServer::start().await;
2012 let client = logged_in_client(Some(server.uri())).await;
2013
2014 let alice = user_id!("@alice:localhost");
2015 let bob = user_id!("@bob:localhost");
2016 let me = user_id!("@example:localhost");
2017
2018 {
2022 let olm_machine = client.olm_machine().await;
2023 let olm_machine = olm_machine.as_ref().unwrap();
2024
2025 olm_machine.update_tracked_users([alice, bob]).await?;
2026
2027 let outgoing_requests = olm_machine.outgoing_requests().await?;
2029
2030 assert_eq!(outgoing_requests.len(), 2);
2031 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
2032 assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
2033
2034 olm_machine
2036 .mark_request_as_sent(
2037 outgoing_requests[0].request_id(),
2038 AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
2039 "one_time_key_counts": {}
2040 }))),
2041 )
2042 .await?;
2043
2044 olm_machine
2045 .mark_request_as_sent(
2046 outgoing_requests[1].request_id(),
2047 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2048 "device_keys": {
2049 alice: {},
2050 bob: {},
2051 }
2052 }))),
2053 )
2054 .await?;
2055
2056 let outgoing_requests = olm_machine.outgoing_requests().await?;
2058
2059 assert_eq!(outgoing_requests.len(), 1);
2060 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
2061
2062 olm_machine
2063 .mark_request_as_sent(
2064 outgoing_requests[0].request_id(),
2065 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2066 "device_keys": {
2067 me: {},
2068 }
2069 }))),
2070 )
2071 .await?;
2072
2073 let outgoing_requests = olm_machine.outgoing_requests().await?;
2075
2076 assert!(outgoing_requests.is_empty());
2077 }
2078
2079 let sync = client
2080 .sliding_sync("test-slidingsync")?
2081 .add_list(SlidingSyncList::builder("new_list"))
2082 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2083 .build()
2084 .await?;
2085
2086 let (_request, _, _) = sync.generate_sync_request().await?;
2088
2089 {
2091 let olm_machine = client.olm_machine().await;
2092 let olm_machine = olm_machine.as_ref().unwrap();
2093
2094 let outgoing_requests = olm_machine.outgoing_requests().await?;
2096
2097 assert_eq!(outgoing_requests.len(), 1);
2098 assert_matches!(
2099 outgoing_requests[0].request(),
2100 AnyOutgoingRequest::KeysQuery(request) => {
2101 assert!(request.device_keys.contains_key(alice));
2102 assert!(request.device_keys.contains_key(bob));
2103 assert!(request.device_keys.contains_key(me));
2104 }
2105 );
2106
2107 olm_machine
2109 .mark_request_as_sent(
2110 outgoing_requests[0].request_id(),
2111 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2112 "device_keys": {
2113 alice: {},
2114 bob: {},
2115 me: {},
2116 }
2117 }))),
2118 )
2119 .await?;
2120 }
2121
2122 sync.set_pos("chocolat".to_owned()).await;
2124
2125 let (_request, _, _) = sync.generate_sync_request().await?;
2126
2127 {
2129 let olm_machine = client.olm_machine().await;
2130 let olm_machine = olm_machine.as_ref().unwrap();
2131
2132 let outgoing_requests = olm_machine.outgoing_requests().await?;
2134
2135 assert!(outgoing_requests.is_empty());
2136 }
2137
2138 Ok(())
2139 }
2140
2141 #[cfg(feature = "e2e-encryption")]
2142 #[async_test]
2143 async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
2144 let server = MockServer::start().await;
2145
2146 #[derive(Deserialize)]
2147 struct PartialRequest {
2148 txn_id: Option<String>,
2149 }
2150
2151 let server_pos = Arc::new(Mutex::new(0));
2152 let _mock_guard = Mock::given(SlidingSyncMatcher)
2153 .respond_with(move |request: &Request| {
2154 let request: PartialRequest = request.body_json().unwrap();
2156 let pos = {
2157 let mut pos = server_pos.lock().unwrap();
2158 let prev = *pos;
2159 *pos += 1;
2160 prev
2161 };
2162
2163 ResponseTemplate::new(200).set_body_json(json!({
2164 "txn_id": request.txn_id,
2165 "pos": pos.to_string(),
2166 }))
2167 })
2168 .mount_as_scoped(&server)
2169 .await;
2170
2171 let client = logged_in_client(Some(server.uri())).await;
2172
2173 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2174
2175 {
2177 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2178
2179 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2180 assert!(request.pos.is_none());
2181 }
2182
2183 let sync = sliding_sync.sync();
2184 let mut sync = pin!(sync);
2185
2186 let next = sync.next().await;
2189 assert_matches!(next, Some(Ok(_update_summary)));
2190
2191 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2192
2193 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2194 .await?
2195 .expect("must have restored fields");
2196
2197 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2200
2201 {
2205 let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2206
2207 let mut position_guard = other_sync.inner.position.lock().await;
2208 position_guard.pos = Some("yolo".to_owned());
2209
2210 other_sync.cache_to_storage(&position_guard).await?;
2211 }
2212
2213 {
2215 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2216 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2217 assert_eq!(request.pos.as_deref(), Some("0"));
2218 }
2219
2220 {
2223 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2224 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2225 }
2226
2227 Ok(())
2228 }
2229
2230 #[cfg(feature = "e2e-encryption")]
2231 #[async_test]
2232 async fn test_sliding_sync_does_remember_pos() -> Result<()> {
2233 let server = MockServer::start().await;
2234
2235 #[derive(Deserialize)]
2236 struct PartialRequest {
2237 txn_id: Option<String>,
2238 }
2239
2240 let server_pos = Arc::new(Mutex::new(0));
2241 let _mock_guard = Mock::given(SlidingSyncMatcher)
2242 .respond_with(move |request: &Request| {
2243 let request: PartialRequest = request.body_json().unwrap();
2245 let pos = {
2246 let mut pos = server_pos.lock().unwrap();
2247 let prev = *pos;
2248 *pos += 1;
2249 prev
2250 };
2251
2252 ResponseTemplate::new(200).set_body_json(json!({
2253 "txn_id": request.txn_id,
2254 "pos": pos.to_string(),
2255 }))
2256 })
2257 .mount_as_scoped(&server)
2258 .await;
2259
2260 let client = logged_in_client(Some(server.uri())).await;
2261
2262 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2263
2264 {
2266 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2267
2268 assert!(request.pos.is_none());
2269 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2270 }
2271
2272 let sync = sliding_sync.sync();
2273 let mut sync = pin!(sync);
2274
2275 let next = sync.next().await;
2278 assert_matches!(next, Some(Ok(_update_summary)));
2279
2280 assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
2281
2282 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2283 .await?
2284 .expect("must have restored fields");
2285
2286 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2289
2290 {
2292 let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
2293
2294 let mut position_guard = other_sync.inner.position.lock().await;
2295 position_guard.pos = Some("42".to_owned());
2296
2297 other_sync.cache_to_storage(&position_guard).await?;
2298 }
2299
2300 {
2302 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2303 assert_eq!(request.pos.as_deref(), Some("42"));
2304 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2305 }
2306
2307 {
2309 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2310 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2311
2312 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2313 assert_eq!(request.pos.as_deref(), Some("42"));
2314 }
2315
2316 sliding_sync.expire_session().await;
2319
2320 {
2321 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2322
2323 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2324 assert!(request.pos.is_none());
2325 }
2326
2327 {
2329 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2330 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2331
2332 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2333 assert!(request.pos.is_none());
2334 }
2335
2336 Ok(())
2337 }
2338
2339 #[async_test]
2340 async fn test_stop_sync_loop() -> Result<()> {
2341 let (_server, sliding_sync) = new_sliding_sync(vec![
2342 SlidingSyncList::builder("foo")
2343 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2344 ])
2345 .await?;
2346
2347 let stream = sliding_sync.sync();
2349 let mut stream = pin!(stream);
2350
2351 assert!(stream.next().await.is_some());
2353
2354 sliding_sync.stop_sync()?;
2356
2357 assert!(stream.next().await.is_none());
2359
2360 let stream = sliding_sync.sync();
2362 let mut stream = pin!(stream);
2363
2364 assert!(stream.next().await.is_some());
2366
2367 Ok(())
2368 }
2369
2370 #[async_test]
2371 async fn test_process_read_receipts() -> Result<()> {
2372 let room = owned_room_id!("!pony:example.org");
2373
2374 let server = MockServer::start().await;
2375 let client = logged_in_client(Some(server.uri())).await;
2376
2377 let sliding_sync = client
2378 .sliding_sync("test")?
2379 .with_receipt_extension(
2380 assign!(http::request::Receipts::default(), { enabled: Some(true) }),
2381 )
2382 .add_list(
2383 SlidingSyncList::builder("all")
2384 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2385 )
2386 .build()
2387 .await?;
2388
2389 {
2391 let server_response = assign!(http::Response::new("0".to_owned()), {
2392 rooms: BTreeMap::from([(
2393 room.clone(),
2394 http::response::Room::default(),
2395 )])
2396 });
2397
2398 let _summary = {
2399 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2400 sliding_sync
2401 .handle_response(
2402 server_response.clone(),
2403 &mut pos_guard,
2404 RequestedRequiredStates::default(),
2405 )
2406 .await?
2407 };
2408 }
2409
2410 let server_response = assign!(http::Response::new("1".to_owned()), {
2411 extensions: assign!(http::response::Extensions::default(), {
2412 receipts: assign!(http::response::Receipts::default(), {
2413 rooms: BTreeMap::from([
2414 (
2415 room.clone(),
2416 Raw::from_json_string(
2417 json!({
2418 "room_id": room,
2419 "type": "m.receipt",
2420 "content": {
2421 "$event:bar.org": {
2422 "m.read": {
2423 client.user_id().unwrap(): {
2424 "ts": 1436451550,
2425 }
2426 }
2427 }
2428 }
2429 })
2430 .to_string(),
2431 ).unwrap()
2432 )
2433 ])
2434 })
2435 })
2436 });
2437
2438 let summary = {
2439 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2440 sliding_sync
2441 .handle_response(
2442 server_response.clone(),
2443 &mut pos_guard,
2444 RequestedRequiredStates::default(),
2445 )
2446 .await?
2447 };
2448
2449 assert!(summary.rooms.contains(&room));
2450
2451 Ok(())
2452 }
2453
2454 #[async_test]
2455 async fn test_process_marked_unread_room_account_data() -> Result<()> {
2456 let room_id = owned_room_id!("!unicorn:example.org");
2457
2458 let server = MockServer::start().await;
2459 let client = logged_in_client(Some(server.uri())).await;
2460
2461 let sliding_sync = client
2464 .sliding_sync("test")?
2465 .with_account_data_extension(
2466 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2467 )
2468 .add_list(
2469 SlidingSyncList::builder("all")
2470 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2471 )
2472 .build()
2473 .await?;
2474
2475 {
2477 let server_response = assign!(http::Response::new("0".to_owned()), {
2478 rooms: BTreeMap::from([(
2479 room_id.clone(),
2480 http::response::Room::default(),
2481 )])
2482 });
2483
2484 let _summary = {
2485 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2486 sliding_sync
2487 .handle_response(
2488 server_response.clone(),
2489 &mut pos_guard,
2490 RequestedRequiredStates::default(),
2491 )
2492 .await?
2493 };
2494 }
2495
2496 let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2500
2501 let update_summary = {
2502 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2503 sliding_sync
2504 .handle_response(
2505 server_response.clone(),
2506 &mut pos_guard,
2507 RequestedRequiredStates::default(),
2508 )
2509 .await?
2510 };
2511
2512 assert!(update_summary.rooms.contains(&room_id));
2515
2516 let room = client.get_room(&room_id).unwrap();
2517
2518 assert!(room.is_marked_unread());
2521
2522 let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2525
2526 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2527 sliding_sync
2528 .handle_response(
2529 server_response.clone(),
2530 &mut pos_guard,
2531 RequestedRequiredStates::default(),
2532 )
2533 .await?;
2534
2535 let room = client.get_room(&room_id).unwrap();
2536
2537 assert!(!room.is_marked_unread());
2538
2539 Ok(())
2540 }
2541
2542 fn make_mark_unread_response(
2543 response_number: &str,
2544 room_id: OwnedRoomId,
2545 unread: bool,
2546 add_rooms_section: bool,
2547 ) -> http::Response {
2548 let rooms = if add_rooms_section {
2549 BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2550 } else {
2551 BTreeMap::new()
2552 };
2553
2554 let extensions = assign!(http::response::Extensions::default(), {
2555 account_data: assign!(http::response::AccountData::default(), {
2556 rooms: BTreeMap::from([
2557 (
2558 room_id,
2559 vec![
2560 Raw::from_json_string(
2561 json!({
2562 "content": {
2563 "unread": unread
2564 },
2565 "type": "m.marked_unread"
2566 })
2567 .to_string(),
2568 ).unwrap()
2569 ]
2570 )
2571 ])
2572 })
2573 });
2574
2575 assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2576 }
2577
2578 #[async_test]
2579 async fn test_process_rooms_account_data() -> Result<()> {
2580 let room = owned_room_id!("!pony:example.org");
2581
2582 let server = MockServer::start().await;
2583 let client = logged_in_client(Some(server.uri())).await;
2584
2585 let sliding_sync = client
2586 .sliding_sync("test")?
2587 .with_account_data_extension(
2588 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2589 )
2590 .add_list(
2591 SlidingSyncList::builder("all")
2592 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2593 )
2594 .build()
2595 .await?;
2596
2597 {
2599 let server_response = assign!(http::Response::new("0".to_owned()), {
2600 rooms: BTreeMap::from([(
2601 room.clone(),
2602 http::response::Room::default(),
2603 )])
2604 });
2605
2606 let _summary = {
2607 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2608 sliding_sync
2609 .handle_response(
2610 server_response.clone(),
2611 &mut pos_guard,
2612 RequestedRequiredStates::default(),
2613 )
2614 .await?
2615 };
2616 }
2617
2618 let server_response = assign!(http::Response::new("1".to_owned()), {
2619 extensions: assign!(http::response::Extensions::default(), {
2620 account_data: assign!(http::response::AccountData::default(), {
2621 rooms: BTreeMap::from([
2622 (
2623 room.clone(),
2624 vec![
2625 Raw::from_json_string(
2626 json!({
2627 "content": {
2628 "tags": {
2629 "u.work": {
2630 "order": 0.9
2631 }
2632 }
2633 },
2634 "type": "m.tag"
2635 })
2636 .to_string(),
2637 ).unwrap()
2638 ]
2639 )
2640 ])
2641 })
2642 })
2643 });
2644 let summary = {
2645 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2646 sliding_sync
2647 .handle_response(
2648 server_response.clone(),
2649 &mut pos_guard,
2650 RequestedRequiredStates::default(),
2651 )
2652 .await?
2653 };
2654
2655 assert!(summary.rooms.contains(&room));
2656
2657 Ok(())
2658 }
2659
2660 #[async_test]
2661 #[cfg(feature = "e2e-encryption")]
2662 async fn test_process_only_encryption_events() -> Result<()> {
2663 use ruma::OneTimeKeyAlgorithm;
2664
2665 let room = owned_room_id!("!croissant:example.org");
2666
2667 let server = MockServer::start().await;
2668 let client = logged_in_client(Some(server.uri())).await;
2669
2670 let server_response = assign!(http::Response::new("0".to_owned()), {
2671 rooms: BTreeMap::from([(
2672 room.clone(),
2673 assign!(http::response::Room::default(), {
2674 name: Some("Croissants lovers".to_owned()),
2675 timeline: Vec::new(),
2676 }),
2677 )]),
2678
2679 extensions: assign!(http::response::Extensions::default(), {
2680 e2ee: assign!(http::response::E2EE::default(), {
2681 device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2682 }),
2683 to_device: Some(assign!(http::response::ToDevice::default(), {
2684 next_batch: "to-device-token".to_owned(),
2685 })),
2686 })
2687 });
2688
2689 let sliding_sync = client
2693 .sliding_sync("test")?
2694 .with_to_device_extension(
2695 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2696 )
2697 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2698 .build()
2699 .await?;
2700
2701 {
2702 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2703
2704 sliding_sync
2705 .handle_response(
2706 server_response.clone(),
2707 &mut position_guard,
2708 RequestedRequiredStates::default(),
2709 )
2710 .await?;
2711 }
2712
2713 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2715 assert_eq!(uploaded_key_count, 42);
2716
2717 {
2718 let olm_machine = &*client.olm_machine_for_testing().await;
2719 assert_eq!(
2720 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2721 Some("to-device-token")
2722 );
2723 }
2724
2725 assert!(client.get_room(&room).is_none());
2727
2728 let client = logged_in_client(Some(server.uri())).await;
2731
2732 let sliding_sync = client
2733 .sliding_sync("test")?
2734 .add_list(SlidingSyncList::builder("thelist"))
2735 .build()
2736 .await?;
2737
2738 {
2739 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2740
2741 sliding_sync
2742 .handle_response(
2743 server_response.clone(),
2744 &mut position_guard,
2745 RequestedRequiredStates::default(),
2746 )
2747 .await?;
2748 }
2749
2750 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2752 assert_eq!(uploaded_key_count, 0);
2753
2754 {
2755 let olm_machine = &*client.olm_machine_for_testing().await;
2756 assert_eq!(
2757 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2758 None
2759 );
2760 }
2761
2762 assert!(client.get_room(&room).is_some());
2764
2765 let client = logged_in_client(Some(server.uri())).await;
2767
2768 let sliding_sync = client
2769 .sliding_sync("test")?
2770 .add_list(SlidingSyncList::builder("thelist"))
2771 .with_to_device_extension(
2772 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2773 )
2774 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2775 .build()
2776 .await?;
2777
2778 {
2779 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2780
2781 sliding_sync
2782 .handle_response(
2783 server_response.clone(),
2784 &mut position_guard,
2785 RequestedRequiredStates::default(),
2786 )
2787 .await?;
2788 }
2789
2790 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2792 assert_eq!(uploaded_key_count, 42);
2793
2794 {
2795 let olm_machine = &*client.olm_machine_for_testing().await;
2796 assert_eq!(
2797 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2798 Some("to-device-token")
2799 );
2800 }
2801
2802 assert!(client.get_room(&room).is_some());
2804
2805 Ok(())
2806 }
2807
2808 #[async_test]
2809 async fn test_lock_multiple_requests() -> Result<()> {
2810 let server = MockServer::start().await;
2811 let client = logged_in_client(Some(server.uri())).await;
2812
2813 let pos = Arc::new(Mutex::new(0));
2814 let _mock_guard = Mock::given(SlidingSyncMatcher)
2815 .respond_with(move |_: &Request| {
2816 let mut pos = pos.lock().unwrap();
2817 *pos += 1;
2818 ResponseTemplate::new(200).set_body_json(json!({
2819 "pos": pos.to_string(),
2820 "lists": {},
2821 "rooms": {}
2822 }))
2823 })
2824 .mount_as_scoped(&server)
2825 .await;
2826
2827 let sliding_sync = client
2828 .sliding_sync("test")?
2829 .with_to_device_extension(
2830 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2831 )
2832 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2833 .build()
2834 .await?;
2835
2836 let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2839
2840 for result in requests.await {
2841 result?;
2842 }
2843
2844 Ok(())
2845 }
2846
2847 #[async_test]
2848 async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2849 let server = MockServer::start().await;
2850 let client = logged_in_client(Some(server.uri())).await;
2851
2852 let pos = Arc::new(Mutex::new(0));
2853 let _mock_guard = Mock::given(SlidingSyncMatcher)
2854 .respond_with(move |_: &Request| {
2855 let mut pos = pos.lock().unwrap();
2856 *pos += 1;
2857 ResponseTemplate::new(200)
2859 .set_body_json(json!({
2860 "pos": pos.to_string(),
2861 "lists": {},
2862 "rooms": {}
2863 }))
2864 .set_delay(Duration::from_secs(2))
2865 })
2866 .mount_as_scoped(&server)
2867 .await;
2868
2869 let sliding_sync =
2870 client
2871 .sliding_sync("test")?
2872 .add_list(SlidingSyncList::builder("room-list").sync_mode(
2873 SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2874 ))
2875 .add_list(
2876 SlidingSyncList::builder("another-list")
2877 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2878 )
2879 .build()
2880 .await?;
2881
2882 let stream = sliding_sync.sync();
2883 let mut stream = pin!(stream);
2884
2885 let cloned_sync = sliding_sync.clone();
2886 spawn(async move {
2887 tokio::time::sleep(Duration::from_millis(100)).await;
2888
2889 cloned_sync
2890 .on_list("another-list", |list| {
2891 list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2892 ready(())
2893 })
2894 .await;
2895 });
2896
2897 assert_matches!(stream.next().await, Some(Ok(_)));
2898
2899 sliding_sync.stop_sync().unwrap();
2900
2901 assert_matches!(stream.next().await, None);
2902
2903 let mut num_requests = 0;
2904
2905 for request in server.received_requests().await.unwrap() {
2906 if !SlidingSyncMatcher.matches(&request) {
2907 continue;
2908 }
2909
2910 let another_list_ranges = if num_requests == 0 {
2911 json!([[0, 10]])
2913 } else {
2914 json!([[10, 20]])
2916 };
2917
2918 num_requests += 1;
2919 assert!(num_requests <= 2, "more than one request hit the server");
2920
2921 let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2922
2923 if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2924 &json_value,
2925 &json!({
2926 "conn_id": "test",
2927 "lists": {
2928 "room-list": {
2929 "ranges": [[0, 9]],
2930 "required_state": [
2931 ["m.room.encryption", ""],
2932 ["m.room.tombstone", ""]
2933 ],
2934 },
2935 "another-list": {
2936 "ranges": another_list_ranges,
2937 "required_state": [
2938 ["m.room.encryption", ""],
2939 ["m.room.tombstone", ""]
2940 ],
2941 },
2942 }
2943 }),
2944 assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2945 ) {
2946 dbg!(json_value);
2947 panic!("json differ: {err}");
2948 }
2949 }
2950
2951 assert_eq!(num_requests, 2);
2952
2953 Ok(())
2954 }
2955
2956 #[async_test]
2957 async fn test_timeout_zero_list() -> Result<()> {
2958 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2959
2960 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2961
2962 assert!(request.timeout.is_some());
2965
2966 Ok(())
2967 }
2968
2969 #[async_test]
2970 async fn test_timeout_one_list() -> Result<()> {
2971 let (_server, sliding_sync) = new_sliding_sync(vec![
2972 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2973 ])
2974 .await?;
2975
2976 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2977
2978 assert!(request.timeout.is_none());
2980
2981 {
2983 let server_response = assign!(http::Response::new("0".to_owned()), {
2984 lists: BTreeMap::from([(
2985 "foo".to_owned(),
2986 assign!(http::response::List::default(), {
2987 count: uint!(7),
2988 })
2989 )])
2990 });
2991
2992 let _summary = {
2993 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2994 sliding_sync
2995 .handle_response(
2996 server_response.clone(),
2997 &mut pos_guard,
2998 RequestedRequiredStates::default(),
2999 )
3000 .await?
3001 };
3002 }
3003
3004 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3005
3006 assert!(request.timeout.is_some());
3008
3009 Ok(())
3010 }
3011
3012 #[async_test]
3013 async fn test_timeout_three_lists() -> Result<()> {
3014 let (_server, sliding_sync) = new_sliding_sync(vec![
3015 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
3016 SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
3017 SlidingSyncList::builder("baz")
3018 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
3019 ])
3020 .await?;
3021
3022 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3023
3024 assert!(request.timeout.is_none());
3026
3027 {
3029 let server_response = assign!(http::Response::new("0".to_owned()), {
3030 lists: BTreeMap::from([(
3031 "foo".to_owned(),
3032 assign!(http::response::List::default(), {
3033 count: uint!(7),
3034 })
3035 )])
3036 });
3037
3038 let _summary = {
3039 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
3040 sliding_sync
3041 .handle_response(
3042 server_response.clone(),
3043 &mut pos_guard,
3044 RequestedRequiredStates::default(),
3045 )
3046 .await?
3047 };
3048 }
3049
3050 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3051
3052 assert!(request.timeout.is_none());
3054
3055 {
3057 let server_response = assign!(http::Response::new("1".to_owned()), {
3058 lists: BTreeMap::from([(
3059 "bar".to_owned(),
3060 assign!(http::response::List::default(), {
3061 count: uint!(7),
3062 })
3063 )])
3064 });
3065
3066 let _summary = {
3067 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
3068 sliding_sync
3069 .handle_response(
3070 server_response.clone(),
3071 &mut pos_guard,
3072 RequestedRequiredStates::default(),
3073 )
3074 .await?
3075 };
3076 }
3077
3078 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3079
3080 assert!(request.timeout.is_some());
3082
3083 Ok(())
3084 }
3085
3086 #[async_test]
3087 async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
3088 let server = MockServer::start().await;
3089 let client = logged_in_client(Some(server.uri())).await;
3090
3091 let _mock_guard = Mock::given(SlidingSyncMatcher)
3092 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3093 "pos": "0",
3094 "lists": {},
3095 "rooms": {}
3096 })))
3097 .mount_as_scoped(&server)
3098 .await;
3099
3100 let sliding_sync = client
3101 .sliding_sync("test")?
3102 .with_to_device_extension(
3103 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
3104 )
3105 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
3106 .build()
3107 .await?;
3108
3109 let sliding_sync = Arc::new(sliding_sync);
3110
3111 let sync_beat_listener = client.inner.sync_beat.listen();
3113 sliding_sync.sync_once().await?;
3114
3115 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
3117 Ok(())
3118 }
3119
3120 #[async_test]
3121 async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
3122 let server = MockServer::start().await;
3123 let client = logged_in_client(Some(server.uri())).await;
3124
3125 let _mock_guard = Mock::given(SlidingSyncMatcher)
3126 .respond_with(ResponseTemplate::new(404))
3127 .mount_as_scoped(&server)
3128 .await;
3129
3130 let sliding_sync = client
3131 .sliding_sync("test")?
3132 .with_to_device_extension(
3133 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
3134 )
3135 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
3136 .build()
3137 .await?;
3138
3139 let sliding_sync = Arc::new(sliding_sync);
3140
3141 let sync_beat_listener = client.inner.sync_beat.listen();
3143 let sync_result = sliding_sync.sync_once().await;
3144 assert!(sync_result.is_err());
3145
3146 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
3148
3149 Ok(())
3150 }
3151
3152 #[async_test]
3153 async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
3154 let server = MatrixMockServer::new().await;
3155 let client = server.client_builder().build().await;
3156 let room_id = room_id!("!mu5hr00m:example.org");
3157
3158 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
3159 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3160 "pos": "0",
3161 "lists": {},
3162 "extensions": {
3163 "account_data": {
3164 "global": [
3165 {
3166 "type": "m.direct",
3167 "content": {
3168 "@de4dlockh0lmes:example.org": [
3169 "!mu5hr00m:example.org"
3170 ]
3171 }
3172 }
3173 ]
3174 }
3175 },
3176 "rooms": {
3177 room_id: {
3178 "name": "Mario Bros Fanbase Room",
3179 "initial": true,
3180 },
3181 }
3182 })))
3183 .mount_as_scoped(server.server())
3184 .await;
3185
3186 let f = EventFactory::new().room(room_id);
3187
3188 Mock::given(method("GET"))
3189 .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
3190 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3191 "chunk": [
3192 f.member(User::Alice).membership(MembershipState::Join).into_raw_timeline(),
3193 ]
3194 })))
3195 .mount(server.server())
3196 .await;
3197
3198 let (tx, rx) = tokio::sync::oneshot::channel();
3199
3200 let tx = Arc::new(Mutex::new(Some(tx)));
3201 client.add_event_handler(move |_: DirectEvent, client: Client| async move {
3202 let members =
3204 client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
3205 assert_eq!(members.len(), 1);
3206 tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
3207 });
3208
3209 let sliding_sync = client
3210 .sliding_sync("test")?
3211 .add_list(SlidingSyncList::builder("thelist"))
3212 .with_account_data_extension(
3213 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
3214 )
3215 .build()
3216 .await?;
3217
3218 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3219 .await
3220 .expect("Sync did not complete in time")
3221 .expect("Sync failed");
3222
3223 tokio::time::timeout(Duration::from_secs(5), rx)
3225 .await
3226 .expect("Event handler did not complete in time")
3227 .expect("Event handler failed");
3228
3229 Ok(())
3230 }
3231
3232 #[cfg(feature = "e2e-encryption")]
3233 #[async_test]
3234 async fn test_syncing_one_time_key_counts_updates() -> Result<()> {
3235 macro_rules! assert_key_count {
3236 ($client: ident, $count:literal) => {{
3237 let machine = $client.olm_machine().await;
3238 let uploaded_key_counts =
3239 machine.as_ref().unwrap().uploaded_key_count().await.unwrap();
3240 assert_eq!(uploaded_key_counts, $count)
3241 }};
3242 }
3243
3244 macro_rules! sync_with_key_count {
3245 ($client: ident, $server:ident, $count:literal) => {
3246 let count = Some($count);
3247 sync_with_key_count!($client, $server, count);
3248 };
3249 ($client: ident, $server:ident, $count:ident) => {{
3250 let count: Option<u32> = $count;
3251
3252 let template = if let Some(count) = count {
3253 ResponseTemplate::new(200).set_body_json(json!({
3254 "pos": "0",
3255 "lists": {},
3256 "extensions": {
3257 "e2ee": {
3258 "device_one_time_keys_count": {
3259 "signed_curve25519": count,
3260 }
3261 }
3262 },
3263 }))
3264 } else {
3265 ResponseTemplate::new(200).set_body_json(json!({
3266 "pos": "0",
3267 "lists": {},
3268 "extensions": {
3269 "e2ee": {}
3270 },
3271 }))
3272 };
3273
3274 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
3275 .respond_with(template)
3276 .mount_as_scoped($server.server())
3277 .await;
3278
3279 let sliding_sync = $client
3280 .sliding_sync("test")?
3281 .with_e2ee_extension(
3282 assign!(http::request::E2EE::default(), { enabled: Some(true)}),
3283 )
3284 .build()
3285 .await?;
3286
3287 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3288 .await
3289 .expect("Sync did not complete in time")
3290 .expect("Sync failed");
3291 }}
3292 }
3293
3294 let server = MatrixMockServer::new().await;
3295 let client = server.client_builder().build().await;
3296
3297 server.mock_upload_keys().ok_with_signed_curve_key_count(50).mock_once().mount().await;
3298
3299 assert_key_count!(client, 0);
3301
3302 sync_with_key_count!(client, server, None);
3304 assert_key_count!(client, 50);
3305
3306 sync_with_key_count!(client, server, None);
3308 assert_key_count!(client, 50);
3309
3310 sync_with_key_count!(client, server, 10);
3312 assert_key_count!(client, 10);
3313
3314 Ok(())
3315 }
3316
3317 #[cfg(feature = "e2e-encryption")]
3325 mod pos_and_event_cache {
3326 use std::time::Duration;
3327
3328 use matrix_sdk_base::{
3329 event_cache::store::{ControlledMemoryStore, EventCacheStore},
3330 store::StoreConfig,
3331 };
3332 use matrix_sdk_common::{
3333 cross_process_lock::CrossProcessLockConfig, executor::spawn, sleep::sleep,
3334 };
3335 use matrix_sdk_test::{async_test, event_factory::EventFactory};
3336 use ruma::{event_id, room_id};
3337 use serde_json::json;
3338 use tokio::time::timeout;
3339 use wiremock::{Mock, ResponseTemplate};
3340
3341 use super::{SlidingSyncMatcher, restore_sliding_sync_state};
3342 use crate::{
3343 Client, Result, SlidingSyncList, sliding_sync::SlidingSync,
3344 test_utils::mocks::MatrixMockServer,
3345 };
3346
3347 async fn set_up(
3350 server: &MatrixMockServer,
3351 store: &ControlledMemoryStore,
3352 ) -> Result<(Client, SlidingSync)> {
3353 let store_config = StoreConfig::new(CrossProcessLockConfig::SingleProcess)
3354 .event_cache_store(store.clone());
3355 let client = server
3356 .client_builder()
3357 .on_builder(|builder| builder.store_config(store_config))
3358 .build()
3359 .await;
3360
3361 let room_id = room_id!("!r0:example.org");
3362 let f = EventFactory::new().room(room_id).sender(ruma::user_id!("@alice:example.org"));
3363
3364 Mock::given(SlidingSyncMatcher)
3365 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3366 "pos": "0",
3367 "lists": {},
3368 "rooms": {
3369 room_id: {
3370 "initial": true,
3371 "timeline": [
3372 f.text_msg("hello").event_id(event_id!("$ev0")).into_raw_sync(),
3373 ],
3374 },
3375 },
3376 })))
3377 .mount(server.server())
3378 .await;
3379
3380 let sliding_sync = client
3381 .sliding_sync("pos-test")?
3382 .add_list(SlidingSyncList::builder("all"))
3383 .build()
3384 .await?;
3385
3386 Ok((client, sliding_sync))
3387 }
3388
3389 async fn persisted_pos(client: &Client, sliding_sync: &SlidingSync) -> Option<String> {
3390 restore_sliding_sync_state(client, &sliding_sync.inner.storage_key)
3391 .await
3392 .unwrap()
3393 .and_then(|fields| fields.pos)
3394 }
3395
3396 #[async_test]
3397 async fn test_pos_is_persisted_after_the_event_cache() -> Result<()> {
3398 let server = MatrixMockServer::new().await;
3399 let store = ControlledMemoryStore::new();
3400 store.block_writes();
3401
3402 let (client, sliding_sync) = set_up(&server, &store).await?;
3403
3404 let sync = spawn({
3405 let sliding_sync = sliding_sync.clone();
3406 async move { sliding_sync.sync_once().await }
3407 });
3408
3409 timeout(Duration::from_secs(5), store.wait_for_write_to_start())
3411 .await
3412 .expect("the event cache never wrote to its store");
3413
3414 sleep(Duration::from_millis(200)).await;
3418 assert!(!sync.is_finished(), "the sync must wait for the event cache store");
3419 assert_eq!(persisted_pos(&client, &sliding_sync).await, None);
3420
3421 store.unblock_writes();
3423 timeout(Duration::from_secs(5), sync)
3424 .await
3425 .expect("the sync never finished after unblocking the store")
3426 .unwrap()?;
3427
3428 assert!(
3429 store.find_event(room_id!("!r0:example.org"), event_id!("$ev0")).await?.is_some()
3430 );
3431 assert_eq!(persisted_pos(&client, &sliding_sync).await.as_deref(), Some("0"));
3432
3433 Ok(())
3434 }
3435 }
3436}