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};
39use ruma::{
40 OwnedRoomId, RoomId,
41 api::{client::sync::sync_events::v5 as http, error::ErrorKind},
42 assign,
43};
44use tokio::{
45 select,
46 sync::{Mutex as AsyncMutex, OwnedMutexGuard, RwLock as AsyncRwLock, broadcast::Sender},
47};
48use tracing::{Instrument, Span, debug, error, info, instrument, trace, warn};
49
50pub use self::{builder::*, client::VersionBuilderError, error::*, list::*};
51use self::{cache::restore_sliding_sync_state, client::SlidingSyncResponseProcessor};
52use crate::{Client, Result, config::RequestConfig};
53
54#[derive(Clone, Debug)]
58pub struct SlidingSync {
59 inner: Arc<SlidingSyncInner>,
61}
62
63#[derive(Debug)]
64pub(super) struct SlidingSyncInner {
65 id: String,
69
70 client: Client,
72
73 poll_timeout: Duration,
75
76 network_timeout: Duration,
79
80 storage_key: String,
82
83 share_pos: bool,
90
91 position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
104
105 lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
107
108 room_subscriptions: StdRwLock<BTreeMap<OwnedRoomId, http::request::RoomSubscription>>,
111
112 extensions: http::request::Extensions,
115
116 internal_channel: Sender<SlidingSyncInternalMessage>,
119}
120
121impl SlidingSync {
122 pub(super) fn new(inner: SlidingSyncInner) -> Self {
123 Self { inner: Arc::new(inner) }
124 }
125
126 async fn cache_to_storage(&self, position: &SlidingSyncPositionMarkers) -> Result<()> {
127 cache::store_sliding_sync_state(self, position).await
128 }
129
130 pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
132 SlidingSyncBuilder::new(id, client)
133 }
134
135 pub fn add_room_subscriptions(
145 &self,
146 room_ids: &[&RoomId],
147 settings: Option<http::request::RoomSubscription>,
148 cancel_in_flight_request: bool,
149 ) {
150 let subscriptions_have_changed = upsert_room_subscriptions(
151 &mut self.inner.room_subscriptions.write().unwrap(),
152 &self.inner.client,
153 room_ids,
154 settings,
155 );
156
157 if cancel_in_flight_request && subscriptions_have_changed {
158 self.inner.cancel_in_flight_request();
159 }
160 }
161
162 pub fn remove_room_subscriptions(&self, room_ids: &[&RoomId], cancel_in_flight_request: bool) {
164 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
165 let mut subscriptions_have_changed = false;
166
167 for room_id in room_ids {
168 if room_subscriptions.remove(*room_id).is_some() {
169 subscriptions_have_changed = true;
170 }
171 }
172
173 if cancel_in_flight_request && subscriptions_have_changed {
174 self.inner.cancel_in_flight_request();
175 }
176 }
177
178 pub fn set_room_subscriptions(
195 &self,
196 room_ids: &[&RoomId],
197 settings: Option<http::request::RoomSubscription>,
198 cancel_in_flight_request: bool,
199 ) {
200 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
201
202 let number_of_subscriptions_before = room_subscriptions.len();
205 room_subscriptions.retain(|room_id, _| room_ids.contains(&room_id.as_ref()));
206 let a_subscription_has_been_removed =
207 room_subscriptions.len() != number_of_subscriptions_before;
208
209 let a_subscription_has_been_added_or_updated = upsert_room_subscriptions(
212 &mut room_subscriptions,
213 &self.inner.client,
214 room_ids,
215 settings,
216 );
217
218 if cancel_in_flight_request
221 && (a_subscription_has_been_added_or_updated || a_subscription_has_been_removed)
222 {
223 self.inner.cancel_in_flight_request();
224 }
225 }
226
227 pub fn reset_and_add_room_subscriptions(
233 &self,
234 room_ids: &[&RoomId],
235 settings: Option<http::request::RoomSubscription>,
236 cancel_in_flight_request: bool,
237 ) {
238 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
239
240 let a_subscription_has_been_removed = !room_subscriptions.is_empty();
241 room_subscriptions.clear();
242
243 let a_subscription_has_been_added = upsert_room_subscriptions(
244 &mut room_subscriptions,
245 &self.inner.client,
246 room_ids,
247 settings,
248 );
249
250 if cancel_in_flight_request
251 && (a_subscription_has_been_added || a_subscription_has_been_removed)
252 {
253 self.inner.cancel_in_flight_request();
254 }
255 }
256
257 pub async fn on_list<Function, FunctionOutput, R>(
259 &self,
260 list_name: &str,
261 function: Function,
262 ) -> Option<R>
263 where
264 Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
265 FunctionOutput: Future<Output = R>,
266 {
267 let lists = self.inner.lists.read().await;
268
269 match lists.get(list_name) {
270 Some(list) => Some(function(list).await),
271 None => None,
272 }
273 }
274
275 pub async fn add_list(
280 &self,
281 list_builder: SlidingSyncListBuilder,
282 ) -> Result<Option<SlidingSyncList>> {
283 let list = list_builder.build(self.inner.internal_channel.clone());
284
285 let old_list = self.inner.lists.write().await.insert(list.name().to_owned(), list);
286
287 self.inner.internal_channel_send_if_possible(
288 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
289 );
290
291 Ok(old_list)
292 }
293
294 pub async fn add_cached_list(
301 &self,
302 mut list_builder: SlidingSyncListBuilder,
303 ) -> Result<Option<SlidingSyncList>> {
304 let _timer = timer!(format!("restoring (loading+processing) list {}", list_builder.name));
305
306 list_builder.set_cached_and_reload(&self.inner.client, &self.inner.storage_key).await?;
307
308 self.add_list(list_builder).await
309 }
310
311 #[instrument(skip_all)]
313 async fn handle_response(
314 &self,
315 mut sliding_sync_response: http::Response,
316 position: &mut SlidingSyncPositionMarkers,
317 requested_required_states: RequestedRequiredStates,
318 ) -> Result<UpdateSummary, crate::Error> {
319 let pos = Some(sliding_sync_response.pos.clone());
320
321 let must_process_rooms_response = self.must_process_rooms_response().await;
322
323 trace!(yes = must_process_rooms_response, "Must process rooms response?");
324
325 let sync_response = {
333 let _timer = timer!("response processor");
334
335 let response_processor = {
336 let state_store_guard = {
340 let _timer = timer!("acquiring the `state_store_lock`");
341
342 self.inner.client.base_client().state_store_lock().lock().await
343 };
344
345 let mut response_processor =
346 SlidingSyncResponseProcessor::new(self.inner.client.clone());
347
348 if self.is_thread_subscriptions_enabled() {
355 response_processor
356 .handle_thread_subscriptions(
357 position.pos.as_deref(),
358 std::mem::take(
359 &mut sliding_sync_response.extensions.thread_subscriptions,
360 ),
361 )
362 .await?;
363 }
364
365 #[cfg(feature = "e2e-encryption")]
366 if self.is_e2ee_enabled() {
367 response_processor
368 .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
369 .await?
370 }
371
372 if must_process_rooms_response {
375 response_processor
376 .handle_room_response(
377 &sliding_sync_response,
378 &requested_required_states,
379 &state_store_guard,
380 )
381 .await?;
382 }
383
384 response_processor
385 };
386
387 response_processor.process_and_take_response().await?
389 };
390
391 debug!("Sliding Sync response has been handled by the client");
392 trace!(?sync_response);
393
394 let update_summary = {
395 let updated_rooms = {
397 let mut updated_rooms = Vec::with_capacity(
398 sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
399 );
400
401 updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
402
403 updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
413
414 updated_rooms
415 };
416
417 let updated_lists = {
419 debug!(
420 lists = ?sliding_sync_response.lists,
421 "Update lists"
422 );
423
424 let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
425 let mut lists = self.inner.lists.write().await;
426
427 for (name, list) in lists.iter_mut() {
431 if let Some(updates) = sliding_sync_response.lists.get(name) {
432 let maximum_number_of_rooms: u32 =
433 updates.count.try_into().expect("failed to convert `count` to `u32`");
434
435 if list.update(Some(maximum_number_of_rooms))? {
436 updated_lists.push(name.clone());
437 }
438 } else if list.update(None)? {
439 updated_lists.push(name.clone());
440 }
441 }
442
443 for name in sliding_sync_response.lists.keys() {
445 if !lists.contains_key(name) {
446 error!("Response for list `{name}` - unknown to us; skipping");
447 }
448 }
449
450 updated_lists
451 };
452
453 UpdateSummary { lists: updated_lists, rooms: updated_rooms }
454 };
455
456 debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
460
461 position.pos = pos;
462
463 #[cfg(feature = "unstable-msc4354")]
466 if let Some(next_batch) = sliding_sync_response.extensions.sticky_events.next_batch.take() {
467 position.sticky_events_since = Some(next_batch);
468 }
469
470 Ok(update_summary)
471 }
472
473 async fn generate_sync_request(
474 &self,
475 ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
476 let mut requests_lists = BTreeMap::new();
478
479 let timeout = {
480 let lists = self.inner.lists.read().await;
481
482 let mut timeout = PollTimeout::Default;
484
485 for (name, list) in lists.iter() {
486 requests_lists.insert(name.clone(), list.next_request()?);
487 timeout = timeout.min(list.requires_timeout());
488 }
489
490 timeout
491 };
492
493 let mut position_guard = {
502 debug!("Waiting to acquire the `position` lock");
503
504 let _timer = timer!("acquiring the `position` lock");
505
506 self.inner.position.clone().lock_owned().await
507 };
508
509 debug!(pos = ?position_guard.pos, "Got a position");
510
511 let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
512
513 let restored_fields = if self.inner.share_pos || to_device_enabled {
514 restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
515 } else {
516 None
517 };
518
519 let pos = if self.inner.share_pos {
522 if let Some(fields) = &restored_fields {
523 if fields.pos != position_guard.pos {
526 info!(
527 "Pos from previous request ('{:?}') was different from \
528 pos in database ('{:?}').",
529 position_guard.pos, fields.pos
530 );
531 position_guard.pos = fields.pos.clone();
532 }
533 fields.pos.clone()
534 } else {
535 position_guard.pos.clone()
536 }
537 } else {
538 position_guard.pos.clone()
539 };
540
541 #[cfg(feature = "e2e-encryption")]
550 if pos.is_none() && self.is_e2ee_enabled() {
551 info!("Marking all tracked users as dirty");
552
553 let olm_machine = self.inner.client.olm_machine().await;
554 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
555 olm_machine.mark_all_tracked_users_as_dirty().await?;
556 }
557
558 let timeout = match timeout {
563 PollTimeout::None => None,
564 PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
565 PollTimeout::Default => Some(self.inner.poll_timeout),
566 };
567
568 Span::current()
569 .record("pos", &pos)
570 .record("timeout", timeout.map(|duration| duration.as_millis()));
571
572 let mut request = assign!(http::Request::new(), {
573 conn_id: Some(self.inner.id.clone()),
574 pos,
575 set_presence: self.inner.client.sync_presence(),
576 timeout,
577 lists: requests_lists,
578 });
579
580 request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
582
583 request.extensions = self.inner.extensions.clone();
585
586 if to_device_enabled {
588 request.extensions.to_device.since =
589 restored_fields.and_then(|fields| fields.to_device_token);
590 }
591
592 #[cfg(feature = "unstable-msc4354")]
594 if self.is_sticky_events_enabled() {
595 request.extensions.sticky_events.since = position_guard.sticky_events_since.clone();
596 }
597
598 Ok((
599 request,
601 RequestConfig::default()
604 .timeout(self.inner.poll_timeout + self.inner.network_timeout)
605 .retry_limit(3),
606 position_guard,
607 ))
608 }
609
610 async fn send_sync_request(
614 &self,
615 request: http::Request,
616 request_config: RequestConfig,
617 mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
618 ) -> Result<UpdateSummary> {
619 debug!("Sending request");
620
621 let requested_required_states = RequestedRequiredStates::from(&request);
623 let request = self.inner.client.send(request).with_request_config(request_config);
624
625 #[cfg(feature = "e2e-encryption")]
633 let response = {
634 if self.is_e2ee_enabled() {
635 let client = self.inner.client.clone();
654 let e2ee_uploads = spawn(
655 async move {
656 if let Err(error) = client.send_outgoing_requests().await {
657 error!(?error, "Error while sending outgoing E2EE requests");
658 }
659 }
660 .instrument(Span::current()),
661 )
662 .abort_on_drop();
665
666 let response = request.await?;
668
669 e2ee_uploads.await.map_err(|error| Error::JoinError {
674 task_description: "e2ee_uploads".to_owned(),
675 error,
676 })?;
677
678 response
679 } else {
680 request.await?
681 }
682 };
683
684 #[cfg(not(feature = "e2e-encryption"))]
687 let response = request.await?;
688
689 debug!("Received response");
690
691 let this = self.clone();
702
703 let future = async move {
706 debug!("Start handling response");
707
708 let updates = this
714 .handle_response(response, &mut position_guard, requested_required_states)
715 .await?;
716
717 this.cache_to_storage(&position_guard).await?;
718
719 drop(position_guard);
722
723 debug!("Done handling response");
724
725 Ok(updates)
726 };
727
728 spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
729 task_description: "handle_response".to_owned(),
730 error,
731 })?
732 }
733
734 #[cfg(feature = "e2e-encryption")]
736 fn is_e2ee_enabled(&self) -> bool {
737 self.inner.extensions.e2ee.enabled == Some(true)
738 }
739
740 fn is_thread_subscriptions_enabled(&self) -> bool {
743 self.inner.extensions.thread_subscriptions.enabled == Some(true)
744 }
745
746 #[cfg(feature = "unstable-msc4354")]
748 fn is_sticky_events_enabled(&self) -> bool {
749 self.inner.extensions.sticky_events.enabled == Some(true)
750 }
751
752 #[cfg(not(feature = "e2e-encryption"))]
753 fn is_e2ee_enabled(&self) -> bool {
754 false
755 }
756
757 async fn must_process_rooms_response(&self) -> bool {
759 !self.inner.room_subscriptions.read().unwrap().is_empty()
762 || !self.inner.lists.read().await.is_empty()
763 }
764
765 #[doc(hidden)]
769 #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
770 pub async fn sync_once(&self) -> Result<UpdateSummary> {
771 let (request, request_config, position_guard) = self.generate_sync_request().await?;
772
773 let summaries = self.send_sync_request(request, request_config, position_guard).await?;
775
776 self.inner.client.inner.sync_beat.notify(usize::MAX);
778
779 Ok(summaries)
780 }
781
782 #[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()))]
792 pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
793 debug!("Starting sync stream");
794
795 let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
796
797 stream! {
798 loop {
799 debug!("Sync stream is running");
800
801 select! {
802 biased;
803
804 internal_message = internal_channel_receiver.recv() => {
805 use SlidingSyncInternalMessage::*;
806
807 debug!(?internal_message, "Sync stream has received an internal message");
808
809 match internal_message {
810 Err(_) | Ok(SyncLoopStop) => {
811 break;
812 }
813
814 Ok(SyncLoopSkipOverCurrentIteration) => {
815 continue;
816 }
817 }
818 }
819
820 update_summary = self.sync_once() => {
821 match update_summary {
822 Ok(updates) => {
823 yield Ok(updates);
824 }
825
826 Err(error) => {
828 if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
829 self.expire_session().await;
831 }
832
833 yield Err(error);
834
835 break;
837 }
838 }
839 }
840 }
841 }
842
843 debug!("Sync stream has exited.");
844 }
845 }
846
847 pub fn stop_sync(&self) -> Result<()> {
856 Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
857 }
858
859 #[doc(hidden)]
870 pub async fn expire_session(&self) {
871 info!("Session expired; resetting `pos`");
872
873 {
874 let lists = self.inner.lists.read().await;
875
876 for list in lists.values() {
877 list.set_maximum_number_of_rooms(None);
879 }
880 }
881
882 {
884 let mut position = self.inner.position.lock().await;
885
886 position.pos = None;
888
889 #[cfg(feature = "unstable-msc4354")]
892 {
893 position.sticky_events_since = None;
894 }
895
896 if let Err(err) = self.cache_to_storage(&position).await {
899 warn!("Failed to invalidate cached sliding sync state: {err}");
900 }
901 }
902
903 {
904 self.inner.room_subscriptions.write().unwrap().clear();
907 }
908 }
909}
910
911fn upsert_room_subscriptions(
920 room_subscriptions: &mut BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
921 client: &Client,
922 room_ids: &[&RoomId],
923 settings: Option<http::request::RoomSubscription>,
924) -> bool {
925 let settings = settings.unwrap_or_default();
926 let mut subscriptions_have_changed = false;
927
928 for room_id in room_ids {
929 match room_subscriptions.entry((*room_id).to_owned()) {
930 Entry::Vacant(entry) => {
931 if let Some(room) = client.get_room(room_id) {
932 room.mark_members_missing();
933 }
934
935 entry.insert(settings.clone());
936
937 subscriptions_have_changed = true;
938 }
939
940 Entry::Occupied(mut entry) => {
943 if room_subscriptions_differ(entry.get(), &settings) {
944 entry.insert(settings.clone());
945
946 subscriptions_have_changed = true;
947 }
948 }
949 }
950 }
951
952 subscriptions_have_changed
953}
954
955fn room_subscriptions_differ(
957 left: &http::request::RoomSubscription,
958 right: &http::request::RoomSubscription,
959) -> bool {
960 left.timeline_limit != right.timeline_limit || left.required_state != right.required_state
961}
962
963impl SlidingSyncInner {
964 #[instrument]
966 fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
967 self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
968 }
969
970 #[instrument]
973 fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
974 let _ = self.internal_channel.send(message);
976 }
977
978 fn cancel_in_flight_request(&self) {
981 self.internal_channel_send_if_possible(
982 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
983 );
984 }
985}
986
987#[derive(Copy, Clone, Debug, PartialEq)]
988enum SlidingSyncInternalMessage {
989 SyncLoopStop,
991
992 SyncLoopSkipOverCurrentIteration,
995}
996
997#[cfg(any(test, feature = "testing"))]
998impl SlidingSync {
999 pub async fn set_pos(&self, new_pos: String) {
1001 let mut position_lock = self.inner.position.lock().await;
1002 position_lock.pos = Some(new_pos);
1003 }
1004}
1005
1006#[derive(Clone, Debug)]
1007pub(super) struct SlidingSyncPositionMarkers {
1008 pos: Option<String>,
1011
1012 #[cfg(feature = "unstable-msc4354")]
1020 sticky_events_since: Option<String>,
1021}
1022
1023#[derive(Debug, Clone)]
1026pub struct UpdateSummary {
1027 pub lists: Vec<String>,
1029 pub rooms: Vec<OwnedRoomId>,
1031}
1032
1033#[derive(Debug)]
1042pub enum PollTimeout {
1043 None,
1045
1046 Some(u32),
1049
1050 Default,
1053}
1054
1055impl PollTimeout {
1056 fn min(self, left: Self) -> Self {
1067 match (self, left) {
1068 (Self::None, _) => Self::None,
1069
1070 (Self::Some(_), Self::None) => Self::None,
1071 (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
1072 (Self::Some(right), Self::Default) => Self::Some(right),
1073
1074 (Self::Default, Self::None) => Self::None,
1075 (Self::Default, Self::Some(left)) => Self::Some(left),
1076 (Self::Default, Self::Default) => Self::Default,
1077 }
1078 }
1079}
1080
1081#[cfg(all(test, not(target_family = "wasm")))]
1082#[allow(clippy::dbg_macro)]
1083mod tests {
1084 use std::{
1085 collections::BTreeMap,
1086 future::ready,
1087 ops::Not,
1088 sync::{Arc, Mutex},
1089 time::Duration,
1090 };
1091
1092 use assert_matches::assert_matches;
1093 use event_listener::Listener;
1094 use futures_util::{StreamExt, future::join_all, pin_mut};
1095 use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1096 use matrix_sdk_common::executor::spawn;
1097 use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1098 use ruma::{
1099 OwnedRoomId, assign,
1100 events::{direct::DirectEvent, room::member::MembershipState},
1101 owned_room_id,
1102 presence::PresenceState,
1103 profile::{
1104 AvatarUrl, DisplayName, ProfileFieldName, UserProfileChanges, UserProfileUpdate,
1105 },
1106 room_id,
1107 serde::Raw,
1108 uint,
1109 };
1110 use serde::Deserialize;
1111 use serde_json::json;
1112 use stream_assert::assert_pending;
1113 use wiremock::{
1114 Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1115 };
1116
1117 use super::{
1118 SlidingSync, SlidingSyncBuilder, SlidingSyncInternalMessage, SlidingSyncList,
1119 SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http,
1120 };
1121 use crate::{
1122 Client, Result,
1123 test_utils::{logged_in_client, mocks::MatrixMockServer},
1124 };
1125
1126 #[derive(Copy, Clone)]
1127 struct SlidingSyncMatcher;
1128
1129 impl Match for SlidingSyncMatcher {
1130 fn matches(&self, request: &Request) -> bool {
1131 request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1132 && request.method == Method::POST
1133 }
1134 }
1135
1136 async fn new_sliding_sync(
1137 lists: Vec<SlidingSyncListBuilder>,
1138 ) -> Result<(MockServer, SlidingSync)> {
1139 let server = MockServer::start().await;
1140 let client = logged_in_client(Some(server.uri())).await;
1141
1142 let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1143
1144 for list in lists {
1145 sliding_sync_builder = sliding_sync_builder.add_list(list);
1146 }
1147
1148 let sliding_sync = sliding_sync_builder.build().await?;
1149
1150 Ok((server, sliding_sync))
1151 }
1152
1153 #[async_test]
1154 async fn test_subscribe_to_own_profile() {
1155 let client = logged_in_client(None).await;
1156 let own_user_id = client.user_id().expect("client should be logged in").to_owned();
1157
1158 let mut response = http::Response::new("0".to_owned());
1161
1162 let mut profile_changes = UserProfileChanges::new();
1163 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Example"));
1164
1165 response
1166 .extensions
1167 .profiles
1168 .users
1169 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1170 client
1171 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1172 .await
1173 .expect("Failed to process sync");
1174
1175 let stream = client.subscribe_to_own_profile().expect("client should be logged in");
1177 pin_mut!(stream);
1178
1179 let profile = stream.next().await.expect("should emit the initial profile");
1180 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1181
1182 let mut response = http::Response::new("1".to_owned());
1184
1185 let mut profile_changes = UserProfileChanges::new();
1186 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Alice"));
1187 response
1188 .extensions
1189 .profiles
1190 .users
1191 .insert(ALICE.to_owned(), UserProfileUpdate::Updated(profile_changes));
1192 client
1193 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1194 .await
1195 .expect("Failed to process sync");
1196
1197 assert_pending!(stream);
1198
1199 let mut response = http::Response::new("2".to_owned());
1201
1202 let mut profile_changes = UserProfileChanges::new();
1203 profile_changes
1204 .updated
1205 .insert(ProfileFieldName::AvatarUrl, json!("mxc://example.org/avatar"));
1206
1207 response
1208 .extensions
1209 .profiles
1210 .users
1211 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1212 client
1213 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1214 .await
1215 .expect("Failed to process sync");
1216
1217 let profile = stream.next().await.expect("should emit the updated profile");
1218 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1219 assert_eq!(
1220 profile.get_static::<AvatarUrl>().unwrap().map(|url| url.to_string()).as_deref(),
1221 Some("mxc://example.org/avatar")
1222 );
1223 }
1224
1225 #[async_test]
1226 async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1227 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1228 let client = sliding_sync.inner.client.clone();
1229
1230 {
1231 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1232
1233 assert_eq!(request.set_presence, PresenceState::Online);
1234 }
1235
1236 client.set_presence(PresenceState::Unavailable, None, false).await?;
1237
1238 {
1239 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1240
1241 assert_eq!(request.set_presence, PresenceState::Unavailable);
1242 }
1243
1244 client.set_presence(PresenceState::Offline, None, false).await?;
1245
1246 {
1247 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1248
1249 assert_eq!(request.set_presence, PresenceState::Offline);
1250 }
1251
1252 Ok(())
1253 }
1254
1255 #[async_test]
1256 async fn test_add_room_subscriptions() -> Result<()> {
1257 let (server, sliding_sync) = new_sliding_sync(vec![
1258 SlidingSyncList::builder("foo")
1259 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1260 ])
1261 .await?;
1262
1263 let stream = sliding_sync.sync();
1264 pin_mut!(stream);
1265
1266 let room_id_0 = room_id!("!r0:bar.org");
1267 let room_id_1 = room_id!("!r1:bar.org");
1268 let room_id_2 = room_id!("!r2:bar.org");
1269
1270 {
1271 let _mock_guard = Mock::given(SlidingSyncMatcher)
1272 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1273 "pos": "1",
1274 "lists": {},
1275 "rooms": {
1276 room_id_0: {
1277 "name": "Room #0",
1278 "initial": true,
1279 },
1280 room_id_1: {
1281 "name": "Room #1",
1282 "initial": true,
1283 },
1284 room_id_2: {
1285 "name": "Room #2",
1286 "initial": true,
1287 },
1288 }
1289 })))
1290 .mount_as_scoped(&server)
1291 .await;
1292
1293 let _ = stream.next().await.unwrap()?;
1294 }
1295
1296 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1297
1298 assert!(room0.are_members_synced().not());
1302
1303 {
1304 struct MemberMatcher(OwnedRoomId);
1305
1306 impl Match for MemberMatcher {
1307 fn matches(&self, request: &Request) -> bool {
1308 request.url.path()
1309 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1310 && request.method == Method::GET
1311 }
1312 }
1313
1314 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1315 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1316 "chunk": [],
1317 })))
1318 .mount_as_scoped(&server)
1319 .await;
1320
1321 assert_matches!(room0.request_members().await, Ok(()));
1322 }
1323
1324 assert!(room0.are_members_synced());
1326
1327 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, true);
1328
1329 assert!(room0.are_members_synced().not());
1332
1333 {
1334 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1335
1336 assert!(room_subscriptions.contains_key(room_id_0));
1337 assert!(room_subscriptions.contains_key(room_id_1));
1338 assert!(!room_subscriptions.contains_key(room_id_2));
1339 }
1340
1341 {
1344 struct MemberMatcher(OwnedRoomId);
1345
1346 impl Match for MemberMatcher {
1347 fn matches(&self, request: &Request) -> bool {
1348 request.url.path()
1349 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1350 && request.method == Method::GET
1351 }
1352 }
1353
1354 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1355 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1356 "chunk": [],
1357 })))
1358 .mount_as_scoped(&server)
1359 .await;
1360
1361 assert_matches!(room0.request_members().await, Ok(()));
1362 }
1363
1364 assert!(room0.are_members_synced());
1366
1367 sliding_sync.add_room_subscriptions(&[room_id_0], None, false);
1368
1369 assert!(room0.are_members_synced());
1372
1373 Ok(())
1374 }
1375
1376 #[async_test]
1377 async fn test_add_remove_and_reset_room_subscriptions() -> Result<()> {
1378 let (_server, sliding_sync) = new_sliding_sync(vec![
1379 SlidingSyncList::builder("foo")
1380 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1381 ])
1382 .await?;
1383
1384 let room_id_0 = room_id!("!r0:bar.org");
1385 let room_id_1 = room_id!("!r1:bar.org");
1386 let room_id_2 = room_id!("!r2:bar.org");
1387 let room_id_3 = room_id!("!r3:bar.org");
1388
1389 {
1391 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1392
1393 assert!(room_subscriptions.is_empty());
1394 }
1395
1396 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1398
1399 {
1400 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1401
1402 assert_eq!(room_subscriptions.len(), 2);
1403 assert!(room_subscriptions.contains_key(room_id_0));
1404 assert!(room_subscriptions.contains_key(room_id_1));
1405 }
1406
1407 sliding_sync.remove_room_subscriptions(&[room_id_0], false);
1409
1410 {
1411 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1412
1413 assert_eq!(room_subscriptions.len(), 1);
1414 assert!(room_subscriptions.contains_key(room_id_1));
1415 }
1416
1417 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1419
1420 {
1421 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1422
1423 assert_eq!(room_subscriptions.len(), 2);
1424 assert!(room_subscriptions.contains_key(room_id_0));
1425 assert!(room_subscriptions.contains_key(room_id_1));
1426 }
1427
1428 sliding_sync.reset_and_add_room_subscriptions(
1430 &[room_id_2, room_id_3],
1431 Default::default(),
1432 false,
1433 );
1434
1435 {
1436 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1437
1438 assert_eq!(room_subscriptions.len(), 2);
1439 assert!(room_subscriptions.contains_key(room_id_2));
1440 assert!(room_subscriptions.contains_key(room_id_3));
1441 }
1442
1443 Ok(())
1444 }
1445
1446 #[async_test]
1447 async fn test_set_room_subscriptions_refreshes_the_settings() -> Result<()> {
1448 let (_server, sliding_sync) = new_sliding_sync(vec![
1449 SlidingSyncList::builder("foo")
1450 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1451 ])
1452 .await?;
1453
1454 let room_id_0 = room_id!("!r0:bar.org");
1455
1456 let settings = |timeline_limit: u32| {
1457 Some(assign!(http::request::RoomSubscription::default(), {
1458 timeline_limit: timeline_limit.into(),
1459 }))
1460 };
1461 let timeline_limit_of_room_0 = || {
1462 sliding_sync
1463 .inner
1464 .room_subscriptions
1465 .read()
1466 .unwrap()
1467 .get(room_id_0)
1468 .map(|subscription| subscription.timeline_limit)
1469 };
1470
1471 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1472
1473 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1475
1476 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1477 assert_matches!(
1478 internal_channel.try_recv(),
1479 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1480 );
1481
1482 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1484
1485 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1486 assert!(internal_channel.try_recv().is_err());
1487
1488 sliding_sync.set_room_subscriptions(&[room_id_0], settings(42), true);
1491
1492 assert_eq!(timeline_limit_of_room_0(), Some(42u32.into()));
1493 assert_matches!(
1494 internal_channel.try_recv(),
1495 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1496 );
1497
1498 Ok(())
1499 }
1500
1501 #[async_test]
1502 async fn test_set_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1503 let (_server, sliding_sync) = new_sliding_sync(vec![
1504 SlidingSyncList::builder("foo")
1505 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1506 ])
1507 .await?;
1508
1509 let room_id_0 = room_id!("!r0:bar.org");
1510 let room_id_1 = room_id!("!r1:bar.org");
1511
1512 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1513
1514 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1517
1518 assert_matches!(
1519 internal_channel.try_recv(),
1520 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1521 );
1522
1523 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1526
1527 assert!(internal_channel.try_recv().is_err());
1528
1529 sliding_sync.set_room_subscriptions(&[room_id_1], None, true);
1531
1532 assert_matches!(
1533 internal_channel.try_recv(),
1534 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1535 );
1536
1537 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1539
1540 assert!(internal_channel.try_recv().is_err());
1541
1542 Ok(())
1543 }
1544
1545 #[async_test]
1546 async fn test_reset_and_add_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1547 let (_server, sliding_sync) = new_sliding_sync(vec![
1548 SlidingSyncList::builder("foo")
1549 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1550 ])
1551 .await?;
1552
1553 let room_id_0 = room_id!("!r0:bar.org");
1554
1555 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1556
1557 sliding_sync.reset_and_add_room_subscriptions(&[], None, true);
1559
1560 assert!(internal_channel.try_recv().is_err());
1561
1562 sliding_sync.reset_and_add_room_subscriptions(&[room_id_0], None, true);
1564
1565 assert_matches!(
1566 internal_channel.try_recv(),
1567 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1568 );
1569
1570 sliding_sync.reset_and_add_room_subscriptions(&[], None, true);
1573
1574 assert!(sliding_sync.inner.room_subscriptions.read().unwrap().is_empty());
1575 assert_matches!(
1576 internal_channel.try_recv(),
1577 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1578 );
1579
1580 sliding_sync.reset_and_add_room_subscriptions(&[room_id_0], None, false);
1582
1583 assert!(internal_channel.try_recv().is_err());
1584
1585 Ok(())
1586 }
1587
1588 #[async_test]
1589 async fn test_set_room_subscriptions() -> Result<()> {
1590 let (server, sliding_sync) = new_sliding_sync(vec![
1591 SlidingSyncList::builder("foo")
1592 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1593 ])
1594 .await?;
1595
1596 let stream = sliding_sync.sync();
1597 pin_mut!(stream);
1598
1599 let room_id_0 = room_id!("!r0:bar.org");
1600 let room_id_1 = room_id!("!r1:bar.org");
1601 let room_id_2 = room_id!("!r2:bar.org");
1602
1603 {
1604 let _mock_guard = Mock::given(SlidingSyncMatcher)
1605 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1606 "pos": "1",
1607 "lists": {},
1608 "rooms": {
1609 room_id_0: {
1610 "name": "Room #0",
1611 "initial": true,
1612 },
1613 room_id_1: {
1614 "name": "Room #1",
1615 "initial": true,
1616 },
1617 room_id_2: {
1618 "name": "Room #2",
1619 "initial": true,
1620 },
1621 }
1622 })))
1623 .mount_as_scoped(&server)
1624 .await;
1625
1626 let _ = stream.next().await.unwrap()?;
1627 }
1628
1629 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1630
1631 assert!(room0.are_members_synced().not());
1635
1636 {
1637 struct MemberMatcher(OwnedRoomId);
1638
1639 impl Match for MemberMatcher {
1640 fn matches(&self, request: &Request) -> bool {
1641 request.url.path()
1642 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1643 && request.method == Method::GET
1644 }
1645 }
1646
1647 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1648 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1649 "chunk": [],
1650 })))
1651 .mount_as_scoped(&server)
1652 .await;
1653
1654 assert_matches!(room0.request_members().await, Ok(()));
1655 }
1656
1657 assert!(room0.are_members_synced());
1659
1660 sliding_sync.set_room_subscriptions(&[room_id_0, room_id_1], None, true);
1661
1662 assert!(room0.are_members_synced().not());
1665
1666 {
1668 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1669
1670 assert!(room_subscriptions.contains_key(room_id_0));
1671 assert!(room_subscriptions.contains_key(room_id_1));
1672 assert!(!room_subscriptions.contains_key(room_id_2));
1673 }
1674
1675 {
1678 struct MemberMatcher(OwnedRoomId);
1679
1680 impl Match for MemberMatcher {
1681 fn matches(&self, request: &Request) -> bool {
1682 request.url.path()
1683 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1684 && request.method == Method::GET
1685 }
1686 }
1687
1688 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1689 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1690 "chunk": [],
1691 })))
1692 .mount_as_scoped(&server)
1693 .await;
1694
1695 assert_matches!(room0.request_members().await, Ok(()));
1696 }
1697
1698 assert!(room0.are_members_synced());
1700
1701 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1702
1703 assert!(room0.are_members_synced());
1706
1707 {
1709 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1710
1711 assert!(room_subscriptions.contains_key(room_id_0));
1712 assert!(!room_subscriptions.contains_key(room_id_1));
1713 assert!(!room_subscriptions.contains_key(room_id_2));
1714 }
1715 Ok(())
1716 }
1717
1718 #[async_test]
1719 async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1720 let (_server, sliding_sync) = new_sliding_sync(vec![
1721 SlidingSyncList::builder("foo")
1722 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1723 ])
1724 .await?;
1725
1726 let room_id_0 = room_id!("!r0:bar.org");
1727 let room_id_1 = room_id!("!r1:bar.org");
1728 let room_id_2 = room_id!("!r2:bar.org");
1729
1730 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, false);
1732
1733 {
1734 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1735
1736 assert!(room_subscriptions.contains_key(room_id_0));
1737 assert!(room_subscriptions.contains_key(room_id_1));
1738 assert!(room_subscriptions.contains_key(room_id_2).not());
1739 }
1740
1741 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1743
1744 {
1745 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1746
1747 assert!(room_subscriptions.contains_key(room_id_0));
1748 assert!(room_subscriptions.contains_key(room_id_1));
1749 assert!(room_subscriptions.contains_key(room_id_2));
1750 }
1751
1752 sliding_sync.expire_session().await;
1754
1755 {
1756 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1757
1758 assert!(room_subscriptions.is_empty());
1759 }
1760
1761 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1763
1764 {
1765 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1766
1767 assert!(room_subscriptions.contains_key(room_id_0).not());
1768 assert!(room_subscriptions.contains_key(room_id_1).not());
1769 assert!(room_subscriptions.contains_key(room_id_2));
1770 }
1771
1772 Ok(())
1773 }
1774
1775 #[async_test]
1776 async fn test_add_list() -> Result<()> {
1777 let (_server, sliding_sync) = new_sliding_sync(vec![
1778 SlidingSyncList::builder("foo")
1779 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1780 ])
1781 .await?;
1782
1783 let _stream = sliding_sync.sync();
1784 pin_mut!(_stream);
1785
1786 sliding_sync
1787 .add_list(
1788 SlidingSyncList::builder("bar")
1789 .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1790 )
1791 .await?;
1792
1793 let lists = sliding_sync.inner.lists.read().await;
1794
1795 assert!(lists.contains_key("foo"));
1796 assert!(lists.contains_key("bar"));
1797
1798 Ok(())
1802 }
1803
1804 #[cfg(feature = "e2e-encryption")]
1805 #[async_test]
1806 async fn test_extensions_to_device_since_is_set() {
1807 use matrix_sdk_base::crypto::store::types::Changes;
1808
1809 let client = logged_in_client(None).await;
1810 let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1811 .unwrap()
1812 .with_to_device_extension(assign!(
1813 http::request::ToDevice::default(),
1814 {
1815 enabled: Some(true),
1816 }
1817 ))
1818 .build()
1819 .await
1820 .unwrap();
1821
1822 {
1824 let to_device = &sliding_sync.inner.extensions.to_device;
1825
1826 assert_eq!(to_device.enabled, Some(true));
1827 assert!(to_device.since.is_none());
1828 }
1829
1830 {
1832 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1833
1834 let to_device = &request.extensions.to_device;
1835
1836 assert_eq!(to_device.enabled, Some(true));
1837 assert!(to_device.since.is_none());
1838 }
1839
1840 let since_token = "depuis".to_owned();
1842
1843 {
1844 if let Some(olm_machine) = &*client.olm_machine().await {
1845 olm_machine
1846 .store()
1847 .save_changes(Changes {
1848 next_batch_token: Some(since_token.clone()),
1849 ..Default::default()
1850 })
1851 .await
1852 .unwrap();
1853 } else {
1854 panic!("Where is the Olm machine?");
1855 }
1856 }
1857
1858 {
1860 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1861
1862 let to_device = &request.extensions.to_device;
1863
1864 assert_eq!(to_device.enabled, Some(true));
1865 assert_eq!(to_device.since, Some(since_token));
1866 }
1867 }
1868
1869 #[cfg(feature = "unstable-msc4354")]
1870 #[async_test]
1871 async fn test_extensions_sticky_events_since_is_set() -> Result<()> {
1872 let server = MockServer::start().await;
1873
1874 #[derive(Deserialize)]
1875 struct PartialRequest {
1876 txn_id: Option<String>,
1877 }
1878
1879 let response_count = Arc::new(Mutex::new(0));
1882 let _mock_guard = Mock::given(SlidingSyncMatcher)
1883 .respond_with(move |request: &Request| {
1884 let request: PartialRequest = request.body_json().unwrap();
1885 let count = {
1886 let mut count = response_count.lock().unwrap();
1887 *count += 1;
1888 *count
1889 };
1890
1891 let mut response = json!({
1892 "txn_id": request.txn_id,
1893 "pos": count.to_string(),
1894 });
1895
1896 if count <= 2 {
1897 response["extensions"] = json!({
1898 "org.matrix.msc4354.sticky_events": {
1899 "next_batch": format!("sticky_{count}"),
1900 }
1901 });
1902 }
1903
1904 ResponseTemplate::new(200).set_body_json(response)
1905 })
1906 .mount_as_scoped(&server)
1907 .await;
1908
1909 let client = logged_in_client(Some(server.uri())).await;
1910
1911 let sliding_sync = client
1912 .sliding_sync("sticky")?
1913 .with_sticky_events_extension(assign!(
1914 http::request::StickyEvents::default(),
1915 { enabled: Some(true) }
1916 ))
1917 .build()
1918 .await?;
1919
1920 {
1922 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1923 assert_eq!(request.extensions.sticky_events.enabled, Some(true));
1924 assert!(request.extensions.sticky_events.since.is_none());
1925 }
1926
1927 sliding_sync.sync_once().await?;
1929
1930 {
1931 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1932 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_1"));
1933 }
1934
1935 sliding_sync.sync_once().await?;
1936
1937 {
1938 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1939 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_2"));
1940 }
1941
1942 sliding_sync.sync_once().await?;
1944
1945 {
1946 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1947 assert_eq!(request.extensions.sticky_events.since.as_deref(), Some("sticky_2"));
1948 }
1949
1950 sliding_sync.expire_session().await;
1952
1953 {
1954 let (request, _, _) = sliding_sync.generate_sync_request().await?;
1955 assert!(request.pos.is_none());
1956 assert!(request.extensions.sticky_events.since.is_none());
1957 }
1958
1959 Ok(())
1960 }
1961
1962 #[async_test]
1968 #[cfg(feature = "e2e-encryption")]
1969 async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
1970 use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
1971 use matrix_sdk_test::ruma_response_from_json;
1972 use ruma::user_id;
1973
1974 let server = MockServer::start().await;
1975 let client = logged_in_client(Some(server.uri())).await;
1976
1977 let alice = user_id!("@alice:localhost");
1978 let bob = user_id!("@bob:localhost");
1979 let me = user_id!("@example:localhost");
1980
1981 {
1985 let olm_machine = client.olm_machine().await;
1986 let olm_machine = olm_machine.as_ref().unwrap();
1987
1988 olm_machine.update_tracked_users([alice, bob]).await?;
1989
1990 let outgoing_requests = olm_machine.outgoing_requests().await?;
1992
1993 assert_eq!(outgoing_requests.len(), 2);
1994 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
1995 assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
1996
1997 olm_machine
1999 .mark_request_as_sent(
2000 outgoing_requests[0].request_id(),
2001 AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
2002 "one_time_key_counts": {}
2003 }))),
2004 )
2005 .await?;
2006
2007 olm_machine
2008 .mark_request_as_sent(
2009 outgoing_requests[1].request_id(),
2010 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2011 "device_keys": {
2012 alice: {},
2013 bob: {},
2014 }
2015 }))),
2016 )
2017 .await?;
2018
2019 let outgoing_requests = olm_machine.outgoing_requests().await?;
2021
2022 assert_eq!(outgoing_requests.len(), 1);
2023 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
2024
2025 olm_machine
2026 .mark_request_as_sent(
2027 outgoing_requests[0].request_id(),
2028 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2029 "device_keys": {
2030 me: {},
2031 }
2032 }))),
2033 )
2034 .await?;
2035
2036 let outgoing_requests = olm_machine.outgoing_requests().await?;
2038
2039 assert!(outgoing_requests.is_empty());
2040 }
2041
2042 let sync = client
2043 .sliding_sync("test-slidingsync")?
2044 .add_list(SlidingSyncList::builder("new_list"))
2045 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2046 .build()
2047 .await?;
2048
2049 let (_request, _, _) = sync.generate_sync_request().await?;
2051
2052 {
2054 let olm_machine = client.olm_machine().await;
2055 let olm_machine = olm_machine.as_ref().unwrap();
2056
2057 let outgoing_requests = olm_machine.outgoing_requests().await?;
2059
2060 assert_eq!(outgoing_requests.len(), 1);
2061 assert_matches!(
2062 outgoing_requests[0].request(),
2063 AnyOutgoingRequest::KeysQuery(request) => {
2064 assert!(request.device_keys.contains_key(alice));
2065 assert!(request.device_keys.contains_key(bob));
2066 assert!(request.device_keys.contains_key(me));
2067 }
2068 );
2069
2070 olm_machine
2072 .mark_request_as_sent(
2073 outgoing_requests[0].request_id(),
2074 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
2075 "device_keys": {
2076 alice: {},
2077 bob: {},
2078 me: {},
2079 }
2080 }))),
2081 )
2082 .await?;
2083 }
2084
2085 sync.set_pos("chocolat".to_owned()).await;
2087
2088 let (_request, _, _) = sync.generate_sync_request().await?;
2089
2090 {
2092 let olm_machine = client.olm_machine().await;
2093 let olm_machine = olm_machine.as_ref().unwrap();
2094
2095 let outgoing_requests = olm_machine.outgoing_requests().await?;
2097
2098 assert!(outgoing_requests.is_empty());
2099 }
2100
2101 Ok(())
2102 }
2103
2104 #[cfg(feature = "e2e-encryption")]
2105 #[async_test]
2106 async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
2107 let server = MockServer::start().await;
2108
2109 #[derive(Deserialize)]
2110 struct PartialRequest {
2111 txn_id: Option<String>,
2112 }
2113
2114 let server_pos = Arc::new(Mutex::new(0));
2115 let _mock_guard = Mock::given(SlidingSyncMatcher)
2116 .respond_with(move |request: &Request| {
2117 let request: PartialRequest = request.body_json().unwrap();
2119 let pos = {
2120 let mut pos = server_pos.lock().unwrap();
2121 let prev = *pos;
2122 *pos += 1;
2123 prev
2124 };
2125
2126 ResponseTemplate::new(200).set_body_json(json!({
2127 "txn_id": request.txn_id,
2128 "pos": pos.to_string(),
2129 }))
2130 })
2131 .mount_as_scoped(&server)
2132 .await;
2133
2134 let client = logged_in_client(Some(server.uri())).await;
2135
2136 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2137
2138 {
2140 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2141
2142 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2143 assert!(request.pos.is_none());
2144 }
2145
2146 let sync = sliding_sync.sync();
2147 pin_mut!(sync);
2148
2149 let next = sync.next().await;
2152 assert_matches!(next, Some(Ok(_update_summary)));
2153
2154 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2155
2156 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2157 .await?
2158 .expect("must have restored fields");
2159
2160 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2163
2164 {
2168 let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2169
2170 let mut position_guard = other_sync.inner.position.lock().await;
2171 position_guard.pos = Some("yolo".to_owned());
2172
2173 other_sync.cache_to_storage(&position_guard).await?;
2174 }
2175
2176 {
2178 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2179 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2180 assert_eq!(request.pos.as_deref(), Some("0"));
2181 }
2182
2183 {
2186 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2187 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2188 }
2189
2190 Ok(())
2191 }
2192
2193 #[cfg(feature = "e2e-encryption")]
2194 #[async_test]
2195 async fn test_sliding_sync_does_remember_pos() -> Result<()> {
2196 let server = MockServer::start().await;
2197
2198 #[derive(Deserialize)]
2199 struct PartialRequest {
2200 txn_id: Option<String>,
2201 }
2202
2203 let server_pos = Arc::new(Mutex::new(0));
2204 let _mock_guard = Mock::given(SlidingSyncMatcher)
2205 .respond_with(move |request: &Request| {
2206 let request: PartialRequest = request.body_json().unwrap();
2208 let pos = {
2209 let mut pos = server_pos.lock().unwrap();
2210 let prev = *pos;
2211 *pos += 1;
2212 prev
2213 };
2214
2215 ResponseTemplate::new(200).set_body_json(json!({
2216 "txn_id": request.txn_id,
2217 "pos": pos.to_string(),
2218 }))
2219 })
2220 .mount_as_scoped(&server)
2221 .await;
2222
2223 let client = logged_in_client(Some(server.uri())).await;
2224
2225 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2226
2227 {
2229 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2230
2231 assert!(request.pos.is_none());
2232 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2233 }
2234
2235 let sync = sliding_sync.sync();
2236 pin_mut!(sync);
2237
2238 let next = sync.next().await;
2241 assert_matches!(next, Some(Ok(_update_summary)));
2242
2243 assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
2244
2245 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2246 .await?
2247 .expect("must have restored fields");
2248
2249 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2252
2253 {
2255 let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
2256
2257 let mut position_guard = other_sync.inner.position.lock().await;
2258 position_guard.pos = Some("42".to_owned());
2259
2260 other_sync.cache_to_storage(&position_guard).await?;
2261 }
2262
2263 {
2265 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2266 assert_eq!(request.pos.as_deref(), Some("42"));
2267 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2268 }
2269
2270 {
2272 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2273 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2274
2275 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2276 assert_eq!(request.pos.as_deref(), Some("42"));
2277 }
2278
2279 sliding_sync.expire_session().await;
2282
2283 {
2284 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2285
2286 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2287 assert!(request.pos.is_none());
2288 }
2289
2290 {
2292 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2293 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2294
2295 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2296 assert!(request.pos.is_none());
2297 }
2298
2299 Ok(())
2300 }
2301
2302 #[async_test]
2303 async fn test_stop_sync_loop() -> Result<()> {
2304 let (_server, sliding_sync) = new_sliding_sync(vec![
2305 SlidingSyncList::builder("foo")
2306 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2307 ])
2308 .await?;
2309
2310 let stream = sliding_sync.sync();
2312 pin_mut!(stream);
2313
2314 assert!(stream.next().await.is_some());
2316
2317 sliding_sync.stop_sync()?;
2319
2320 assert!(stream.next().await.is_none());
2322
2323 let stream = sliding_sync.sync();
2325 pin_mut!(stream);
2326
2327 assert!(stream.next().await.is_some());
2329
2330 Ok(())
2331 }
2332
2333 #[async_test]
2334 async fn test_process_read_receipts() -> Result<()> {
2335 let room = owned_room_id!("!pony:example.org");
2336
2337 let server = MockServer::start().await;
2338 let client = logged_in_client(Some(server.uri())).await;
2339 client.event_cache().subscribe().unwrap();
2340
2341 let sliding_sync = client
2342 .sliding_sync("test")?
2343 .with_receipt_extension(
2344 assign!(http::request::Receipts::default(), { enabled: Some(true) }),
2345 )
2346 .add_list(
2347 SlidingSyncList::builder("all")
2348 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2349 )
2350 .build()
2351 .await?;
2352
2353 {
2355 let server_response = assign!(http::Response::new("0".to_owned()), {
2356 rooms: BTreeMap::from([(
2357 room.clone(),
2358 http::response::Room::default(),
2359 )])
2360 });
2361
2362 let _summary = {
2363 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2364 sliding_sync
2365 .handle_response(
2366 server_response.clone(),
2367 &mut pos_guard,
2368 RequestedRequiredStates::default(),
2369 )
2370 .await?
2371 };
2372 }
2373
2374 let server_response = assign!(http::Response::new("1".to_owned()), {
2375 extensions: assign!(http::response::Extensions::default(), {
2376 receipts: assign!(http::response::Receipts::default(), {
2377 rooms: BTreeMap::from([
2378 (
2379 room.clone(),
2380 Raw::from_json_string(
2381 json!({
2382 "room_id": room,
2383 "type": "m.receipt",
2384 "content": {
2385 "$event:bar.org": {
2386 "m.read": {
2387 client.user_id().unwrap(): {
2388 "ts": 1436451550,
2389 }
2390 }
2391 }
2392 }
2393 })
2394 .to_string(),
2395 ).unwrap()
2396 )
2397 ])
2398 })
2399 })
2400 });
2401
2402 let summary = {
2403 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2404 sliding_sync
2405 .handle_response(
2406 server_response.clone(),
2407 &mut pos_guard,
2408 RequestedRequiredStates::default(),
2409 )
2410 .await?
2411 };
2412
2413 assert!(summary.rooms.contains(&room));
2414
2415 Ok(())
2416 }
2417
2418 #[async_test]
2419 async fn test_process_marked_unread_room_account_data() -> Result<()> {
2420 let room_id = owned_room_id!("!unicorn:example.org");
2421
2422 let server = MockServer::start().await;
2423 let client = logged_in_client(Some(server.uri())).await;
2424
2425 let sliding_sync = client
2428 .sliding_sync("test")?
2429 .with_account_data_extension(
2430 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2431 )
2432 .add_list(
2433 SlidingSyncList::builder("all")
2434 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2435 )
2436 .build()
2437 .await?;
2438
2439 {
2441 let server_response = assign!(http::Response::new("0".to_owned()), {
2442 rooms: BTreeMap::from([(
2443 room_id.clone(),
2444 http::response::Room::default(),
2445 )])
2446 });
2447
2448 let _summary = {
2449 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2450 sliding_sync
2451 .handle_response(
2452 server_response.clone(),
2453 &mut pos_guard,
2454 RequestedRequiredStates::default(),
2455 )
2456 .await?
2457 };
2458 }
2459
2460 let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2464
2465 let update_summary = {
2466 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2467 sliding_sync
2468 .handle_response(
2469 server_response.clone(),
2470 &mut pos_guard,
2471 RequestedRequiredStates::default(),
2472 )
2473 .await?
2474 };
2475
2476 assert!(update_summary.rooms.contains(&room_id));
2479
2480 let room = client.get_room(&room_id).unwrap();
2481
2482 assert!(room.is_marked_unread());
2485
2486 let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2489
2490 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2491 sliding_sync
2492 .handle_response(
2493 server_response.clone(),
2494 &mut pos_guard,
2495 RequestedRequiredStates::default(),
2496 )
2497 .await?;
2498
2499 let room = client.get_room(&room_id).unwrap();
2500
2501 assert!(!room.is_marked_unread());
2502
2503 Ok(())
2504 }
2505
2506 fn make_mark_unread_response(
2507 response_number: &str,
2508 room_id: OwnedRoomId,
2509 unread: bool,
2510 add_rooms_section: bool,
2511 ) -> http::Response {
2512 let rooms = if add_rooms_section {
2513 BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2514 } else {
2515 BTreeMap::new()
2516 };
2517
2518 let extensions = assign!(http::response::Extensions::default(), {
2519 account_data: assign!(http::response::AccountData::default(), {
2520 rooms: BTreeMap::from([
2521 (
2522 room_id,
2523 vec![
2524 Raw::from_json_string(
2525 json!({
2526 "content": {
2527 "unread": unread
2528 },
2529 "type": "m.marked_unread"
2530 })
2531 .to_string(),
2532 ).unwrap()
2533 ]
2534 )
2535 ])
2536 })
2537 });
2538
2539 assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2540 }
2541
2542 #[async_test]
2543 async fn test_process_rooms_account_data() -> Result<()> {
2544 let room = owned_room_id!("!pony:example.org");
2545
2546 let server = MockServer::start().await;
2547 let client = logged_in_client(Some(server.uri())).await;
2548
2549 let sliding_sync = client
2550 .sliding_sync("test")?
2551 .with_account_data_extension(
2552 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2553 )
2554 .add_list(
2555 SlidingSyncList::builder("all")
2556 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2557 )
2558 .build()
2559 .await?;
2560
2561 {
2563 let server_response = assign!(http::Response::new("0".to_owned()), {
2564 rooms: BTreeMap::from([(
2565 room.clone(),
2566 http::response::Room::default(),
2567 )])
2568 });
2569
2570 let _summary = {
2571 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2572 sliding_sync
2573 .handle_response(
2574 server_response.clone(),
2575 &mut pos_guard,
2576 RequestedRequiredStates::default(),
2577 )
2578 .await?
2579 };
2580 }
2581
2582 let server_response = assign!(http::Response::new("1".to_owned()), {
2583 extensions: assign!(http::response::Extensions::default(), {
2584 account_data: assign!(http::response::AccountData::default(), {
2585 rooms: BTreeMap::from([
2586 (
2587 room.clone(),
2588 vec![
2589 Raw::from_json_string(
2590 json!({
2591 "content": {
2592 "tags": {
2593 "u.work": {
2594 "order": 0.9
2595 }
2596 }
2597 },
2598 "type": "m.tag"
2599 })
2600 .to_string(),
2601 ).unwrap()
2602 ]
2603 )
2604 ])
2605 })
2606 })
2607 });
2608 let summary = {
2609 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2610 sliding_sync
2611 .handle_response(
2612 server_response.clone(),
2613 &mut pos_guard,
2614 RequestedRequiredStates::default(),
2615 )
2616 .await?
2617 };
2618
2619 assert!(summary.rooms.contains(&room));
2620
2621 Ok(())
2622 }
2623
2624 #[async_test]
2625 #[cfg(feature = "e2e-encryption")]
2626 async fn test_process_only_encryption_events() -> Result<()> {
2627 use ruma::OneTimeKeyAlgorithm;
2628
2629 let room = owned_room_id!("!croissant:example.org");
2630
2631 let server = MockServer::start().await;
2632 let client = logged_in_client(Some(server.uri())).await;
2633
2634 let server_response = assign!(http::Response::new("0".to_owned()), {
2635 rooms: BTreeMap::from([(
2636 room.clone(),
2637 assign!(http::response::Room::default(), {
2638 name: Some("Croissants lovers".to_owned()),
2639 timeline: Vec::new(),
2640 }),
2641 )]),
2642
2643 extensions: assign!(http::response::Extensions::default(), {
2644 e2ee: assign!(http::response::E2EE::default(), {
2645 device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2646 }),
2647 to_device: Some(assign!(http::response::ToDevice::default(), {
2648 next_batch: "to-device-token".to_owned(),
2649 })),
2650 })
2651 });
2652
2653 let sliding_sync = client
2657 .sliding_sync("test")?
2658 .with_to_device_extension(
2659 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2660 )
2661 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2662 .build()
2663 .await?;
2664
2665 {
2666 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2667
2668 sliding_sync
2669 .handle_response(
2670 server_response.clone(),
2671 &mut position_guard,
2672 RequestedRequiredStates::default(),
2673 )
2674 .await?;
2675 }
2676
2677 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2679 assert_eq!(uploaded_key_count, 42);
2680
2681 {
2682 let olm_machine = &*client.olm_machine_for_testing().await;
2683 assert_eq!(
2684 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2685 Some("to-device-token")
2686 );
2687 }
2688
2689 assert!(client.get_room(&room).is_none());
2691
2692 let client = logged_in_client(Some(server.uri())).await;
2695
2696 let sliding_sync = client
2697 .sliding_sync("test")?
2698 .add_list(SlidingSyncList::builder("thelist"))
2699 .build()
2700 .await?;
2701
2702 {
2703 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2704
2705 sliding_sync
2706 .handle_response(
2707 server_response.clone(),
2708 &mut position_guard,
2709 RequestedRequiredStates::default(),
2710 )
2711 .await?;
2712 }
2713
2714 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2716 assert_eq!(uploaded_key_count, 0);
2717
2718 {
2719 let olm_machine = &*client.olm_machine_for_testing().await;
2720 assert_eq!(
2721 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2722 None
2723 );
2724 }
2725
2726 assert!(client.get_room(&room).is_some());
2728
2729 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 .with_to_device_extension(
2736 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2737 )
2738 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2739 .build()
2740 .await?;
2741
2742 {
2743 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2744
2745 sliding_sync
2746 .handle_response(
2747 server_response.clone(),
2748 &mut position_guard,
2749 RequestedRequiredStates::default(),
2750 )
2751 .await?;
2752 }
2753
2754 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2756 assert_eq!(uploaded_key_count, 42);
2757
2758 {
2759 let olm_machine = &*client.olm_machine_for_testing().await;
2760 assert_eq!(
2761 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2762 Some("to-device-token")
2763 );
2764 }
2765
2766 assert!(client.get_room(&room).is_some());
2768
2769 Ok(())
2770 }
2771
2772 #[async_test]
2773 async fn test_lock_multiple_requests() -> Result<()> {
2774 let server = MockServer::start().await;
2775 let client = logged_in_client(Some(server.uri())).await;
2776
2777 let pos = Arc::new(Mutex::new(0));
2778 let _mock_guard = Mock::given(SlidingSyncMatcher)
2779 .respond_with(move |_: &Request| {
2780 let mut pos = pos.lock().unwrap();
2781 *pos += 1;
2782 ResponseTemplate::new(200).set_body_json(json!({
2783 "pos": pos.to_string(),
2784 "lists": {},
2785 "rooms": {}
2786 }))
2787 })
2788 .mount_as_scoped(&server)
2789 .await;
2790
2791 let sliding_sync = client
2792 .sliding_sync("test")?
2793 .with_to_device_extension(
2794 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2795 )
2796 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2797 .build()
2798 .await?;
2799
2800 let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2803
2804 for result in requests.await {
2805 result?;
2806 }
2807
2808 Ok(())
2809 }
2810
2811 #[async_test]
2812 async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2813 let server = MockServer::start().await;
2814 let client = logged_in_client(Some(server.uri())).await;
2815
2816 let pos = Arc::new(Mutex::new(0));
2817 let _mock_guard = Mock::given(SlidingSyncMatcher)
2818 .respond_with(move |_: &Request| {
2819 let mut pos = pos.lock().unwrap();
2820 *pos += 1;
2821 ResponseTemplate::new(200)
2823 .set_body_json(json!({
2824 "pos": pos.to_string(),
2825 "lists": {},
2826 "rooms": {}
2827 }))
2828 .set_delay(Duration::from_secs(2))
2829 })
2830 .mount_as_scoped(&server)
2831 .await;
2832
2833 let sliding_sync =
2834 client
2835 .sliding_sync("test")?
2836 .add_list(SlidingSyncList::builder("room-list").sync_mode(
2837 SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2838 ))
2839 .add_list(
2840 SlidingSyncList::builder("another-list")
2841 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2842 )
2843 .build()
2844 .await?;
2845
2846 let stream = sliding_sync.sync();
2847 pin_mut!(stream);
2848
2849 let cloned_sync = sliding_sync.clone();
2850 spawn(async move {
2851 tokio::time::sleep(Duration::from_millis(100)).await;
2852
2853 cloned_sync
2854 .on_list("another-list", |list| {
2855 list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2856 ready(())
2857 })
2858 .await;
2859 });
2860
2861 assert_matches!(stream.next().await, Some(Ok(_)));
2862
2863 sliding_sync.stop_sync().unwrap();
2864
2865 assert_matches!(stream.next().await, None);
2866
2867 let mut num_requests = 0;
2868
2869 for request in server.received_requests().await.unwrap() {
2870 if !SlidingSyncMatcher.matches(&request) {
2871 continue;
2872 }
2873
2874 let another_list_ranges = if num_requests == 0 {
2875 json!([[0, 10]])
2877 } else {
2878 json!([[10, 20]])
2880 };
2881
2882 num_requests += 1;
2883 assert!(num_requests <= 2, "more than one request hit the server");
2884
2885 let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2886
2887 if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2888 &json_value,
2889 &json!({
2890 "conn_id": "test",
2891 "lists": {
2892 "room-list": {
2893 "ranges": [[0, 9]],
2894 "required_state": [
2895 ["m.room.encryption", ""],
2896 ["m.room.tombstone", ""]
2897 ],
2898 },
2899 "another-list": {
2900 "ranges": another_list_ranges,
2901 "required_state": [
2902 ["m.room.encryption", ""],
2903 ["m.room.tombstone", ""]
2904 ],
2905 },
2906 }
2907 }),
2908 assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2909 ) {
2910 dbg!(json_value);
2911 panic!("json differ: {err}");
2912 }
2913 }
2914
2915 assert_eq!(num_requests, 2);
2916
2917 Ok(())
2918 }
2919
2920 #[async_test]
2921 async fn test_timeout_zero_list() -> Result<()> {
2922 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2923
2924 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2925
2926 assert!(request.timeout.is_some());
2929
2930 Ok(())
2931 }
2932
2933 #[async_test]
2934 async fn test_timeout_one_list() -> Result<()> {
2935 let (_server, sliding_sync) = new_sliding_sync(vec![
2936 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2937 ])
2938 .await?;
2939
2940 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2941
2942 assert!(request.timeout.is_none());
2944
2945 {
2947 let server_response = assign!(http::Response::new("0".to_owned()), {
2948 lists: BTreeMap::from([(
2949 "foo".to_owned(),
2950 assign!(http::response::List::default(), {
2951 count: uint!(7),
2952 })
2953 )])
2954 });
2955
2956 let _summary = {
2957 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2958 sliding_sync
2959 .handle_response(
2960 server_response.clone(),
2961 &mut pos_guard,
2962 RequestedRequiredStates::default(),
2963 )
2964 .await?
2965 };
2966 }
2967
2968 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2969
2970 assert!(request.timeout.is_some());
2972
2973 Ok(())
2974 }
2975
2976 #[async_test]
2977 async fn test_timeout_three_lists() -> Result<()> {
2978 let (_server, sliding_sync) = new_sliding_sync(vec![
2979 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2980 SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
2981 SlidingSyncList::builder("baz")
2982 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2983 ])
2984 .await?;
2985
2986 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2987
2988 assert!(request.timeout.is_none());
2990
2991 {
2993 let server_response = assign!(http::Response::new("0".to_owned()), {
2994 lists: BTreeMap::from([(
2995 "foo".to_owned(),
2996 assign!(http::response::List::default(), {
2997 count: uint!(7),
2998 })
2999 )])
3000 });
3001
3002 let _summary = {
3003 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
3004 sliding_sync
3005 .handle_response(
3006 server_response.clone(),
3007 &mut pos_guard,
3008 RequestedRequiredStates::default(),
3009 )
3010 .await?
3011 };
3012 }
3013
3014 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3015
3016 assert!(request.timeout.is_none());
3018
3019 {
3021 let server_response = assign!(http::Response::new("1".to_owned()), {
3022 lists: BTreeMap::from([(
3023 "bar".to_owned(),
3024 assign!(http::response::List::default(), {
3025 count: uint!(7),
3026 })
3027 )])
3028 });
3029
3030 let _summary = {
3031 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
3032 sliding_sync
3033 .handle_response(
3034 server_response.clone(),
3035 &mut pos_guard,
3036 RequestedRequiredStates::default(),
3037 )
3038 .await?
3039 };
3040 }
3041
3042 let (request, _, _) = sliding_sync.generate_sync_request().await?;
3043
3044 assert!(request.timeout.is_some());
3046
3047 Ok(())
3048 }
3049
3050 #[async_test]
3051 async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
3052 let server = MockServer::start().await;
3053 let client = logged_in_client(Some(server.uri())).await;
3054
3055 let _mock_guard = Mock::given(SlidingSyncMatcher)
3056 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3057 "pos": "0",
3058 "lists": {},
3059 "rooms": {}
3060 })))
3061 .mount_as_scoped(&server)
3062 .await;
3063
3064 let sliding_sync = client
3065 .sliding_sync("test")?
3066 .with_to_device_extension(
3067 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
3068 )
3069 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
3070 .build()
3071 .await?;
3072
3073 let sliding_sync = Arc::new(sliding_sync);
3074
3075 let sync_beat_listener = client.inner.sync_beat.listen();
3077 sliding_sync.sync_once().await?;
3078
3079 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
3081 Ok(())
3082 }
3083
3084 #[async_test]
3085 async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
3086 let server = MockServer::start().await;
3087 let client = logged_in_client(Some(server.uri())).await;
3088
3089 let _mock_guard = Mock::given(SlidingSyncMatcher)
3090 .respond_with(ResponseTemplate::new(404))
3091 .mount_as_scoped(&server)
3092 .await;
3093
3094 let sliding_sync = client
3095 .sliding_sync("test")?
3096 .with_to_device_extension(
3097 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
3098 )
3099 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
3100 .build()
3101 .await?;
3102
3103 let sliding_sync = Arc::new(sliding_sync);
3104
3105 let sync_beat_listener = client.inner.sync_beat.listen();
3107 let sync_result = sliding_sync.sync_once().await;
3108 assert!(sync_result.is_err());
3109
3110 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
3112
3113 Ok(())
3114 }
3115
3116 #[async_test]
3117 async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
3118 let server = MatrixMockServer::new().await;
3119 let client = server.client_builder().build().await;
3120 let room_id = room_id!("!mu5hr00m:example.org");
3121
3122 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
3123 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3124 "pos": "0",
3125 "lists": {},
3126 "extensions": {
3127 "account_data": {
3128 "global": [
3129 {
3130 "type": "m.direct",
3131 "content": {
3132 "@de4dlockh0lmes:example.org": [
3133 "!mu5hr00m:example.org"
3134 ]
3135 }
3136 }
3137 ]
3138 }
3139 },
3140 "rooms": {
3141 room_id: {
3142 "name": "Mario Bros Fanbase Room",
3143 "initial": true,
3144 },
3145 }
3146 })))
3147 .mount_as_scoped(server.server())
3148 .await;
3149
3150 let f = EventFactory::new().room(room_id);
3151
3152 Mock::given(method("GET"))
3153 .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
3154 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3155 "chunk": [
3156 f.member(&ALICE).membership(MembershipState::Join).into_raw_timeline(),
3157 ]
3158 })))
3159 .mount(server.server())
3160 .await;
3161
3162 let (tx, rx) = tokio::sync::oneshot::channel();
3163
3164 let tx = Arc::new(Mutex::new(Some(tx)));
3165 client.add_event_handler(move |_: DirectEvent, client: Client| async move {
3166 let members =
3168 client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
3169 assert_eq!(members.len(), 1);
3170 tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
3171 });
3172
3173 let sliding_sync = client
3174 .sliding_sync("test")?
3175 .add_list(SlidingSyncList::builder("thelist"))
3176 .with_account_data_extension(
3177 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
3178 )
3179 .build()
3180 .await?;
3181
3182 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3183 .await
3184 .expect("Sync did not complete in time")
3185 .expect("Sync failed");
3186
3187 tokio::time::timeout(Duration::from_secs(5), rx)
3189 .await
3190 .expect("Event handler did not complete in time")
3191 .expect("Event handler failed");
3192
3193 Ok(())
3194 }
3195
3196 #[cfg(feature = "e2e-encryption")]
3197 #[async_test]
3198 async fn test_syncing_one_time_key_counts_updates() -> Result<()> {
3199 macro_rules! assert_key_count {
3200 ($client: ident, $count:literal) => {{
3201 let machine = $client.olm_machine().await;
3202 let uploaded_key_counts =
3203 machine.as_ref().unwrap().uploaded_key_count().await.unwrap();
3204 assert_eq!(uploaded_key_counts, $count)
3205 }};
3206 }
3207
3208 macro_rules! sync_with_key_count {
3209 ($client: ident, $server:ident, $count:literal) => {
3210 let count = Some($count);
3211 sync_with_key_count!($client, $server, count);
3212 };
3213 ($client: ident, $server:ident, $count:ident) => {{
3214 let count: Option<u32> = $count;
3215
3216 let template = if let Some(count) = count {
3217 ResponseTemplate::new(200).set_body_json(json!({
3218 "pos": "0",
3219 "lists": {},
3220 "extensions": {
3221 "e2ee": {
3222 "device_one_time_keys_count": {
3223 "signed_curve25519": count,
3224 }
3225 }
3226 },
3227 }))
3228 } else {
3229 ResponseTemplate::new(200).set_body_json(json!({
3230 "pos": "0",
3231 "lists": {},
3232 "extensions": {
3233 "e2ee": {}
3234 },
3235 }))
3236 };
3237
3238 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
3239 .respond_with(template)
3240 .mount_as_scoped($server.server())
3241 .await;
3242
3243 let sliding_sync = $client
3244 .sliding_sync("test")?
3245 .with_e2ee_extension(
3246 assign!(http::request::E2EE::default(), { enabled: Some(true)}),
3247 )
3248 .build()
3249 .await?;
3250
3251 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3252 .await
3253 .expect("Sync did not complete in time")
3254 .expect("Sync failed");
3255 }}
3256 }
3257
3258 let server = MatrixMockServer::new().await;
3259 let client = server.client_builder().build().await;
3260
3261 server.mock_upload_keys().ok_with_signed_curve_key_count(50).mock_once().mount().await;
3262
3263 assert_key_count!(client, 0);
3265
3266 sync_with_key_count!(client, server, None);
3268 assert_key_count!(client, 50);
3269
3270 sync_with_key_count!(client, server, None);
3272 assert_key_count!(client, 50);
3273
3274 sync_with_key_count!(client, server, 10);
3276 assert_key_count!(client, 10);
3277
3278 Ok(())
3279 }
3280}