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();
204 room_subscriptions.retain(|room_id, _| room_ids.contains(&room_id.as_ref()));
205 let a_subscription_has_been_removed =
206 room_subscriptions.len() != number_of_subscriptions_before;
207
208 let a_subscription_has_been_added_or_updated = upsert_room_subscriptions(
211 &mut room_subscriptions,
212 &self.inner.client,
213 room_ids,
214 settings,
215 );
216
217 if cancel_in_flight_request
220 && (a_subscription_has_been_added_or_updated || a_subscription_has_been_removed)
221 {
222 self.inner.cancel_in_flight_request();
223 }
224 }
225
226 pub fn reset_and_add_room_subscriptions(
232 &self,
233 room_ids: &[&RoomId],
234 settings: Option<http::request::RoomSubscription>,
235 cancel_in_flight_request: bool,
236 ) {
237 let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
238
239 let a_subscription_has_been_removed = !room_subscriptions.is_empty();
240 room_subscriptions.clear();
241
242 let a_subscription_has_been_added = upsert_room_subscriptions(
243 &mut room_subscriptions,
244 &self.inner.client,
245 room_ids,
246 settings,
247 );
248
249 if cancel_in_flight_request
250 && (a_subscription_has_been_added || a_subscription_has_been_removed)
251 {
252 self.inner.cancel_in_flight_request();
253 }
254 }
255
256 pub async fn on_list<Function, FunctionOutput, R>(
258 &self,
259 list_name: &str,
260 function: Function,
261 ) -> Option<R>
262 where
263 Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
264 FunctionOutput: Future<Output = R>,
265 {
266 let lists = self.inner.lists.read().await;
267
268 match lists.get(list_name) {
269 Some(list) => Some(function(list).await),
270 None => None,
271 }
272 }
273
274 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 = {
339 let _timer = timer!("acquiring the `state_store_lock`");
340
341 self.inner.client.base_client().state_store_lock().lock().await
342 };
343
344 let mut response_processor =
345 SlidingSyncResponseProcessor::new(self.inner.client.clone());
346
347 if self.is_thread_subscriptions_enabled() {
353 response_processor
354 .handle_thread_subscriptions(
355 position.pos.as_deref(),
356 std::mem::take(
357 &mut sliding_sync_response.extensions.thread_subscriptions,
358 ),
359 )
360 .await?;
361 }
362
363 #[cfg(feature = "e2e-encryption")]
364 if self.is_e2ee_enabled() {
365 response_processor
366 .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
367 .await?
368 }
369
370 if must_process_rooms_response {
373 response_processor
374 .handle_room_response(
375 &sliding_sync_response,
376 &requested_required_states,
377 &state_store_guard,
378 )
379 .await?;
380 }
381
382 response_processor
383 };
384
385 response_processor.process_and_take_response().await?
387 };
388
389 debug!("Sliding Sync response has been handled by the client");
390 trace!(?sync_response);
391
392 let update_summary = {
393 let updated_rooms = {
395 let mut updated_rooms = Vec::with_capacity(
396 sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
397 );
398
399 updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
400
401 updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
409
410 updated_rooms
411 };
412
413 let updated_lists = {
415 debug!(
416 lists = ?sliding_sync_response.lists,
417 "Update lists"
418 );
419
420 let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
421 let mut lists = self.inner.lists.write().await;
422
423 for (name, list) in lists.iter_mut() {
426 if let Some(updates) = sliding_sync_response.lists.get(name) {
427 let maximum_number_of_rooms: u32 =
428 updates.count.try_into().expect("failed to convert `count` to `u32`");
429
430 if list.update(Some(maximum_number_of_rooms))? {
431 updated_lists.push(name.clone());
432 }
433 } else if list.update(None)? {
434 updated_lists.push(name.clone());
435 }
436 }
437
438 for name in sliding_sync_response.lists.keys() {
440 if !lists.contains_key(name) {
441 error!("Response for list `{name}` - unknown to us; skipping");
442 }
443 }
444
445 updated_lists
446 };
447
448 UpdateSummary { lists: updated_lists, rooms: updated_rooms }
449 };
450
451 debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
455
456 position.pos = pos;
457
458 Ok(update_summary)
459 }
460
461 async fn generate_sync_request(
462 &self,
463 ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
464 let mut requests_lists = BTreeMap::new();
466
467 let timeout = {
468 let lists = self.inner.lists.read().await;
469
470 let mut timeout = PollTimeout::Default;
472
473 for (name, list) in lists.iter() {
474 requests_lists.insert(name.clone(), list.next_request()?);
475 timeout = timeout.min(list.requires_timeout());
476 }
477
478 timeout
479 };
480
481 let mut position_guard = {
489 debug!("Waiting to acquire the `position` lock");
490
491 let _timer = timer!("acquiring the `position` lock");
492
493 self.inner.position.clone().lock_owned().await
494 };
495
496 debug!(pos = ?position_guard.pos, "Got a position");
497
498 let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
499
500 let restored_fields = if self.inner.share_pos || to_device_enabled {
501 restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
502 } else {
503 None
504 };
505
506 let pos = if self.inner.share_pos {
509 if let Some(fields) = &restored_fields {
510 if fields.pos != position_guard.pos {
512 info!(
513 "Pos from previous request ('{:?}') was different from \
514 pos in database ('{:?}').",
515 position_guard.pos, fields.pos
516 );
517 position_guard.pos = fields.pos.clone();
518 }
519 fields.pos.clone()
520 } else {
521 position_guard.pos.clone()
522 }
523 } else {
524 position_guard.pos.clone()
525 };
526
527 #[cfg(feature = "e2e-encryption")]
536 if pos.is_none() && self.is_e2ee_enabled() {
537 info!("Marking all tracked users as dirty");
538
539 let olm_machine = self.inner.client.olm_machine().await;
540 let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
541 olm_machine.mark_all_tracked_users_as_dirty().await?;
542 }
543
544 let timeout = match timeout {
549 PollTimeout::None => None,
550 PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
551 PollTimeout::Default => Some(self.inner.poll_timeout),
552 };
553
554 Span::current()
555 .record("pos", &pos)
556 .record("timeout", timeout.map(|duration| duration.as_millis()));
557
558 let mut request = assign!(http::Request::new(), {
559 conn_id: Some(self.inner.id.clone()),
560 pos,
561 set_presence: self.inner.client.sync_presence(),
562 timeout,
563 lists: requests_lists,
564 });
565
566 request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
568
569 request.extensions = self.inner.extensions.clone();
571
572 if to_device_enabled {
574 request.extensions.to_device.since =
575 restored_fields.and_then(|fields| fields.to_device_token);
576 }
577
578 Ok((
579 request,
581 RequestConfig::default()
584 .timeout(self.inner.poll_timeout + self.inner.network_timeout)
585 .retry_limit(3),
586 position_guard,
587 ))
588 }
589
590 async fn send_sync_request(
594 &self,
595 request: http::Request,
596 request_config: RequestConfig,
597 mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
598 ) -> Result<UpdateSummary> {
599 debug!("Sending request");
600
601 let requested_required_states = RequestedRequiredStates::from(&request);
603 let request = self.inner.client.send(request).with_request_config(request_config);
604
605 #[cfg(feature = "e2e-encryption")]
612 let response = {
613 if self.is_e2ee_enabled() {
614 let client = self.inner.client.clone();
631 let e2ee_uploads = spawn(
632 async move {
633 if let Err(error) = client.send_outgoing_requests().await {
634 error!(?error, "Error while sending outgoing E2EE requests");
635 }
636 }
637 .instrument(Span::current()),
638 )
639 .abort_on_drop();
642
643 let response = request.await?;
645
646 e2ee_uploads.await.map_err(|error| Error::JoinError {
651 task_description: "e2ee_uploads".to_owned(),
652 error,
653 })?;
654
655 response
656 } else {
657 request.await?
658 }
659 };
660
661 #[cfg(not(feature = "e2e-encryption"))]
663 let response = request.await?;
664
665 debug!("Received response");
666
667 let this = self.clone();
677
678 let future = async move {
681 debug!("Start handling response");
682
683 let updates = this
689 .handle_response(response, &mut position_guard, requested_required_states)
690 .await?;
691
692 this.cache_to_storage(&position_guard).await?;
693
694 drop(position_guard);
697
698 debug!("Done handling response");
699
700 Ok(updates)
701 };
702
703 spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
704 task_description: "handle_response".to_owned(),
705 error,
706 })?
707 }
708
709 #[cfg(feature = "e2e-encryption")]
711 fn is_e2ee_enabled(&self) -> bool {
712 self.inner.extensions.e2ee.enabled == Some(true)
713 }
714
715 fn is_thread_subscriptions_enabled(&self) -> bool {
718 self.inner.extensions.thread_subscriptions.enabled == Some(true)
719 }
720
721 #[cfg(not(feature = "e2e-encryption"))]
722 fn is_e2ee_enabled(&self) -> bool {
723 false
724 }
725
726 async fn must_process_rooms_response(&self) -> bool {
728 !self.inner.room_subscriptions.read().unwrap().is_empty()
731 || !self.inner.lists.read().await.is_empty()
732 }
733
734 #[doc(hidden)]
738 #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
739 pub async fn sync_once(&self) -> Result<UpdateSummary> {
740 let (request, request_config, position_guard) = self.generate_sync_request().await?;
741
742 let summaries = self.send_sync_request(request, request_config, position_guard).await?;
744
745 self.inner.client.inner.sync_beat.notify(usize::MAX);
747
748 Ok(summaries)
749 }
750
751 #[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()))]
761 pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
762 debug!("Starting sync stream");
763
764 let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
765
766 stream! {
767 loop {
768 debug!("Sync stream is running");
769
770 select! {
771 biased;
772
773 internal_message = internal_channel_receiver.recv() => {
774 use SlidingSyncInternalMessage::*;
775
776 debug!(?internal_message, "Sync stream has received an internal message");
777
778 match internal_message {
779 Err(_) | Ok(SyncLoopStop) => {
780 break;
781 }
782
783 Ok(SyncLoopSkipOverCurrentIteration) => {
784 continue;
785 }
786 }
787 }
788
789 update_summary = self.sync_once() => {
790 match update_summary {
791 Ok(updates) => {
792 yield Ok(updates);
793 }
794
795 Err(error) => {
797 if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
798 self.expire_session().await;
800 }
801
802 yield Err(error);
803
804 break;
806 }
807 }
808 }
809 }
810 }
811
812 debug!("Sync stream has exited.");
813 }
814 }
815
816 pub fn stop_sync(&self) -> Result<()> {
825 Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
826 }
827
828 #[doc(hidden)]
839 pub async fn expire_session(&self) {
840 info!("Session expired; resetting `pos`");
841
842 {
843 let lists = self.inner.lists.read().await;
844
845 for list in lists.values() {
846 list.set_maximum_number_of_rooms(None);
848 }
849 }
850
851 {
853 let mut position = self.inner.position.lock().await;
854
855 position.pos = None;
857
858 if let Err(err) = self.cache_to_storage(&position).await {
862 warn!("Failed to invalidate cached sliding sync state: {err}");
863 }
864 }
865
866 {
867 self.inner.room_subscriptions.write().unwrap().clear();
870 }
871 }
872}
873
874fn upsert_room_subscriptions(
883 room_subscriptions: &mut BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
884 client: &Client,
885 room_ids: &[&RoomId],
886 settings: Option<http::request::RoomSubscription>,
887) -> bool {
888 let settings = settings.unwrap_or_default();
889 let mut subscriptions_have_changed = false;
890
891 for room_id in room_ids {
892 match room_subscriptions.entry((*room_id).to_owned()) {
893 Entry::Vacant(entry) => {
894 if let Some(room) = client.get_room(room_id) {
895 room.mark_members_missing();
896 }
897
898 entry.insert(settings.clone());
899
900 subscriptions_have_changed = true;
901 }
902
903 Entry::Occupied(mut entry) => {
906 if room_subscriptions_differ(entry.get(), &settings) {
907 entry.insert(settings.clone());
908
909 subscriptions_have_changed = true;
910 }
911 }
912 }
913 }
914
915 subscriptions_have_changed
916}
917
918fn room_subscriptions_differ(
920 left: &http::request::RoomSubscription,
921 right: &http::request::RoomSubscription,
922) -> bool {
923 left.timeline_limit != right.timeline_limit || left.required_state != right.required_state
924}
925
926impl SlidingSyncInner {
927 #[instrument]
929 fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
930 self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
931 }
932
933 #[instrument]
936 fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
937 let _ = self.internal_channel.send(message);
939 }
940
941 fn cancel_in_flight_request(&self) {
944 self.internal_channel_send_if_possible(
945 SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
946 );
947 }
948}
949
950#[derive(Copy, Clone, Debug, PartialEq)]
951enum SlidingSyncInternalMessage {
952 SyncLoopStop,
954
955 SyncLoopSkipOverCurrentIteration,
958}
959
960#[cfg(any(test, feature = "testing"))]
961impl SlidingSync {
962 pub async fn set_pos(&self, new_pos: String) {
964 let mut position_lock = self.inner.position.lock().await;
965 position_lock.pos = Some(new_pos);
966 }
967}
968
969#[derive(Clone, Debug)]
970pub(super) struct SlidingSyncPositionMarkers {
971 pos: Option<String>,
974}
975
976#[derive(Debug, Clone)]
979pub struct UpdateSummary {
980 pub lists: Vec<String>,
982 pub rooms: Vec<OwnedRoomId>,
984}
985
986#[derive(Debug)]
995pub enum PollTimeout {
996 None,
998
999 Some(u32),
1002
1003 Default,
1006}
1007
1008impl PollTimeout {
1009 fn min(self, left: Self) -> Self {
1020 match (self, left) {
1021 (Self::None, _) => Self::None,
1022
1023 (Self::Some(_), Self::None) => Self::None,
1024 (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
1025 (Self::Some(right), Self::Default) => Self::Some(right),
1026
1027 (Self::Default, Self::None) => Self::None,
1028 (Self::Default, Self::Some(left)) => Self::Some(left),
1029 (Self::Default, Self::Default) => Self::Default,
1030 }
1031 }
1032}
1033
1034#[cfg(all(test, not(target_family = "wasm")))]
1035#[allow(clippy::dbg_macro)]
1036mod tests {
1037 use std::{
1038 collections::BTreeMap,
1039 future::ready,
1040 ops::Not,
1041 sync::{Arc, Mutex},
1042 time::Duration,
1043 };
1044
1045 use assert_matches::assert_matches;
1046 use event_listener::Listener;
1047 use futures_util::{StreamExt, future::join_all, pin_mut};
1048 use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1049 use matrix_sdk_common::executor::spawn;
1050 use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1051 use ruma::{
1052 OwnedRoomId, assign,
1053 events::{direct::DirectEvent, room::member::MembershipState},
1054 owned_room_id,
1055 presence::PresenceState,
1056 profile::{
1057 AvatarUrl, DisplayName, ProfileFieldName, UserProfileChanges, UserProfileUpdate,
1058 },
1059 room_id,
1060 serde::Raw,
1061 uint,
1062 };
1063 use serde::Deserialize;
1064 use serde_json::json;
1065 use stream_assert::assert_pending;
1066 use wiremock::{
1067 Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1068 };
1069
1070 use super::{
1071 SlidingSync, SlidingSyncBuilder, SlidingSyncInternalMessage, SlidingSyncList,
1072 SlidingSyncListBuilder, SlidingSyncMode, cache::restore_sliding_sync_state, http,
1073 };
1074 use crate::{
1075 Client, Result,
1076 test_utils::{logged_in_client, mocks::MatrixMockServer},
1077 };
1078
1079 #[derive(Copy, Clone)]
1080 struct SlidingSyncMatcher;
1081
1082 impl Match for SlidingSyncMatcher {
1083 fn matches(&self, request: &Request) -> bool {
1084 request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1085 && request.method == Method::POST
1086 }
1087 }
1088
1089 async fn new_sliding_sync(
1090 lists: Vec<SlidingSyncListBuilder>,
1091 ) -> Result<(MockServer, SlidingSync)> {
1092 let server = MockServer::start().await;
1093 let client = logged_in_client(Some(server.uri())).await;
1094
1095 let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1096
1097 for list in lists {
1098 sliding_sync_builder = sliding_sync_builder.add_list(list);
1099 }
1100
1101 let sliding_sync = sliding_sync_builder.build().await?;
1102
1103 Ok((server, sliding_sync))
1104 }
1105
1106 #[async_test]
1107 async fn test_subscribe_to_own_profile() {
1108 let client = logged_in_client(None).await;
1109 let own_user_id = client.user_id().expect("client should be logged in").to_owned();
1110
1111 let mut response = http::Response::new("0".to_owned());
1114
1115 let mut profile_changes = UserProfileChanges::new();
1116 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Example"));
1117
1118 response
1119 .extensions
1120 .profiles
1121 .users
1122 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1123 client
1124 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1125 .await
1126 .expect("Failed to process sync");
1127
1128 let stream = client.subscribe_to_own_profile().expect("client should be logged in");
1130 pin_mut!(stream);
1131
1132 let profile = stream.next().await.expect("should emit the initial profile");
1133 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1134
1135 let mut response = http::Response::new("1".to_owned());
1137
1138 let mut profile_changes = UserProfileChanges::new();
1139 profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Alice"));
1140 response
1141 .extensions
1142 .profiles
1143 .users
1144 .insert(ALICE.to_owned(), UserProfileUpdate::Updated(profile_changes));
1145 client
1146 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1147 .await
1148 .expect("Failed to process sync");
1149
1150 assert_pending!(stream);
1151
1152 let mut response = http::Response::new("2".to_owned());
1154
1155 let mut profile_changes = UserProfileChanges::new();
1156 profile_changes
1157 .updated
1158 .insert(ProfileFieldName::AvatarUrl, json!("mxc://example.org/avatar"));
1159
1160 response
1161 .extensions
1162 .profiles
1163 .users
1164 .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1165 client
1166 .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1167 .await
1168 .expect("Failed to process sync");
1169
1170 let profile = stream.next().await.expect("should emit the updated profile");
1171 assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1172 assert_eq!(
1173 profile.get_static::<AvatarUrl>().unwrap().map(|url| url.to_string()).as_deref(),
1174 Some("mxc://example.org/avatar")
1175 );
1176 }
1177
1178 #[async_test]
1179 async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1180 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1181 let client = sliding_sync.inner.client.clone();
1182
1183 {
1184 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1185
1186 assert_eq!(request.set_presence, PresenceState::Online);
1187 }
1188
1189 client.set_presence(PresenceState::Unavailable, None, false).await?;
1190
1191 {
1192 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1193
1194 assert_eq!(request.set_presence, PresenceState::Unavailable);
1195 }
1196
1197 client.set_presence(PresenceState::Offline, None, false).await?;
1198
1199 {
1200 let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1201
1202 assert_eq!(request.set_presence, PresenceState::Offline);
1203 }
1204
1205 Ok(())
1206 }
1207
1208 #[async_test]
1209 async fn test_add_room_subscriptions() -> Result<()> {
1210 let (server, sliding_sync) = new_sliding_sync(vec![
1211 SlidingSyncList::builder("foo")
1212 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1213 ])
1214 .await?;
1215
1216 let stream = sliding_sync.sync();
1217 pin_mut!(stream);
1218
1219 let room_id_0 = room_id!("!r0:bar.org");
1220 let room_id_1 = room_id!("!r1:bar.org");
1221 let room_id_2 = room_id!("!r2:bar.org");
1222
1223 {
1224 let _mock_guard = Mock::given(SlidingSyncMatcher)
1225 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1226 "pos": "1",
1227 "lists": {},
1228 "rooms": {
1229 room_id_0: {
1230 "name": "Room #0",
1231 "initial": true,
1232 },
1233 room_id_1: {
1234 "name": "Room #1",
1235 "initial": true,
1236 },
1237 room_id_2: {
1238 "name": "Room #2",
1239 "initial": true,
1240 },
1241 }
1242 })))
1243 .mount_as_scoped(&server)
1244 .await;
1245
1246 let _ = stream.next().await.unwrap()?;
1247 }
1248
1249 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1250
1251 assert!(room0.are_members_synced().not());
1255
1256 {
1257 struct MemberMatcher(OwnedRoomId);
1258
1259 impl Match for MemberMatcher {
1260 fn matches(&self, request: &Request) -> bool {
1261 request.url.path()
1262 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1263 && request.method == Method::GET
1264 }
1265 }
1266
1267 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1268 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1269 "chunk": [],
1270 })))
1271 .mount_as_scoped(&server)
1272 .await;
1273
1274 assert_matches!(room0.request_members().await, Ok(()));
1275 }
1276
1277 assert!(room0.are_members_synced());
1279
1280 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, true);
1281
1282 assert!(room0.are_members_synced().not());
1285
1286 {
1287 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1288
1289 assert!(room_subscriptions.contains_key(room_id_0));
1290 assert!(room_subscriptions.contains_key(room_id_1));
1291 assert!(!room_subscriptions.contains_key(room_id_2));
1292 }
1293
1294 {
1297 struct MemberMatcher(OwnedRoomId);
1298
1299 impl Match for MemberMatcher {
1300 fn matches(&self, request: &Request) -> bool {
1301 request.url.path()
1302 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1303 && request.method == Method::GET
1304 }
1305 }
1306
1307 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1308 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1309 "chunk": [],
1310 })))
1311 .mount_as_scoped(&server)
1312 .await;
1313
1314 assert_matches!(room0.request_members().await, Ok(()));
1315 }
1316
1317 assert!(room0.are_members_synced());
1319
1320 sliding_sync.add_room_subscriptions(&[room_id_0], None, false);
1321
1322 assert!(room0.are_members_synced());
1325
1326 Ok(())
1327 }
1328
1329 #[async_test]
1330 async fn test_add_remove_and_reset_room_subscriptions() -> Result<()> {
1331 let (_server, sliding_sync) = new_sliding_sync(vec![
1332 SlidingSyncList::builder("foo")
1333 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1334 ])
1335 .await?;
1336
1337 let room_id_0 = room_id!("!r0:bar.org");
1338 let room_id_1 = room_id!("!r1:bar.org");
1339 let room_id_2 = room_id!("!r2:bar.org");
1340 let room_id_3 = room_id!("!r3:bar.org");
1341
1342 {
1344 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1345
1346 assert!(room_subscriptions.is_empty());
1347 }
1348
1349 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1351
1352 {
1353 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1354
1355 assert_eq!(room_subscriptions.len(), 2);
1356 assert!(room_subscriptions.contains_key(room_id_0));
1357 assert!(room_subscriptions.contains_key(room_id_1));
1358 }
1359
1360 sliding_sync.remove_room_subscriptions(&[room_id_0], false);
1362
1363 {
1364 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1365
1366 assert_eq!(room_subscriptions.len(), 1);
1367 assert!(room_subscriptions.contains_key(room_id_1));
1368 }
1369
1370 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], Default::default(), false);
1372
1373 {
1374 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1375
1376 assert_eq!(room_subscriptions.len(), 2);
1377 assert!(room_subscriptions.contains_key(room_id_0));
1378 assert!(room_subscriptions.contains_key(room_id_1));
1379 }
1380
1381 sliding_sync.reset_and_add_room_subscriptions(
1383 &[room_id_2, room_id_3],
1384 Default::default(),
1385 false,
1386 );
1387
1388 {
1389 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1390
1391 assert_eq!(room_subscriptions.len(), 2);
1392 assert!(room_subscriptions.contains_key(room_id_2));
1393 assert!(room_subscriptions.contains_key(room_id_3));
1394 }
1395
1396 Ok(())
1397 }
1398
1399 #[async_test]
1400 async fn test_set_room_subscriptions_refreshes_the_settings() -> Result<()> {
1401 let (_server, sliding_sync) = new_sliding_sync(vec![
1402 SlidingSyncList::builder("foo")
1403 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1404 ])
1405 .await?;
1406
1407 let room_id_0 = room_id!("!r0:bar.org");
1408
1409 let settings = |timeline_limit: u32| {
1410 Some(assign!(http::request::RoomSubscription::default(), {
1411 timeline_limit: timeline_limit.into(),
1412 }))
1413 };
1414 let timeline_limit_of_room_0 = || {
1415 sliding_sync
1416 .inner
1417 .room_subscriptions
1418 .read()
1419 .unwrap()
1420 .get(room_id_0)
1421 .map(|subscription| subscription.timeline_limit)
1422 };
1423
1424 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1425
1426 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1428
1429 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1430 assert_matches!(
1431 internal_channel.try_recv(),
1432 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1433 );
1434
1435 sliding_sync.set_room_subscriptions(&[room_id_0], settings(10), true);
1437
1438 assert_eq!(timeline_limit_of_room_0(), Some(10u32.into()));
1439 assert!(internal_channel.try_recv().is_err());
1440
1441 sliding_sync.set_room_subscriptions(&[room_id_0], settings(42), true);
1444
1445 assert_eq!(timeline_limit_of_room_0(), Some(42u32.into()));
1446 assert_matches!(
1447 internal_channel.try_recv(),
1448 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1449 );
1450
1451 Ok(())
1452 }
1453
1454 #[async_test]
1455 async fn test_set_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1456 let (_server, sliding_sync) = new_sliding_sync(vec![
1457 SlidingSyncList::builder("foo")
1458 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1459 ])
1460 .await?;
1461
1462 let room_id_0 = room_id!("!r0:bar.org");
1463 let room_id_1 = room_id!("!r1:bar.org");
1464
1465 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1466
1467 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1470
1471 assert_matches!(
1472 internal_channel.try_recv(),
1473 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1474 );
1475
1476 sliding_sync.set_room_subscriptions(&[room_id_0], None, true);
1479
1480 assert!(internal_channel.try_recv().is_err());
1481
1482 sliding_sync.set_room_subscriptions(&[room_id_1], None, true);
1484
1485 assert_matches!(
1486 internal_channel.try_recv(),
1487 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1488 );
1489
1490 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1492
1493 assert!(internal_channel.try_recv().is_err());
1494
1495 Ok(())
1496 }
1497
1498 #[async_test]
1499 async fn test_reset_and_add_room_subscriptions_cancels_the_in_flight_request() -> Result<()> {
1500 let (_server, sliding_sync) = new_sliding_sync(vec![
1501 SlidingSyncList::builder("foo")
1502 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1503 ])
1504 .await?;
1505
1506 let room_id_0 = room_id!("!r0:bar.org");
1507
1508 let mut internal_channel = sliding_sync.inner.internal_channel.subscribe();
1509
1510 sliding_sync.reset_and_add_room_subscriptions(&[], None, true);
1512
1513 assert!(internal_channel.try_recv().is_err());
1514
1515 sliding_sync.reset_and_add_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.reset_and_add_room_subscriptions(&[], None, true);
1525
1526 assert!(sliding_sync.inner.room_subscriptions.read().unwrap().is_empty());
1527 assert_matches!(
1528 internal_channel.try_recv(),
1529 Ok(SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration)
1530 );
1531
1532 sliding_sync.reset_and_add_room_subscriptions(&[room_id_0], None, false);
1534
1535 assert!(internal_channel.try_recv().is_err());
1536
1537 Ok(())
1538 }
1539
1540 #[async_test]
1541 async fn test_set_room_subscriptions() -> Result<()> {
1542 let (server, sliding_sync) = new_sliding_sync(vec![
1543 SlidingSyncList::builder("foo")
1544 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1545 ])
1546 .await?;
1547
1548 let stream = sliding_sync.sync();
1549 pin_mut!(stream);
1550
1551 let room_id_0 = room_id!("!r0:bar.org");
1552 let room_id_1 = room_id!("!r1:bar.org");
1553 let room_id_2 = room_id!("!r2:bar.org");
1554
1555 {
1556 let _mock_guard = Mock::given(SlidingSyncMatcher)
1557 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1558 "pos": "1",
1559 "lists": {},
1560 "rooms": {
1561 room_id_0: {
1562 "name": "Room #0",
1563 "initial": true,
1564 },
1565 room_id_1: {
1566 "name": "Room #1",
1567 "initial": true,
1568 },
1569 room_id_2: {
1570 "name": "Room #2",
1571 "initial": true,
1572 },
1573 }
1574 })))
1575 .mount_as_scoped(&server)
1576 .await;
1577
1578 let _ = stream.next().await.unwrap()?;
1579 }
1580
1581 let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1582
1583 assert!(room0.are_members_synced().not());
1587
1588 {
1589 struct MemberMatcher(OwnedRoomId);
1590
1591 impl Match for MemberMatcher {
1592 fn matches(&self, request: &Request) -> bool {
1593 request.url.path()
1594 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1595 && request.method == Method::GET
1596 }
1597 }
1598
1599 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1600 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1601 "chunk": [],
1602 })))
1603 .mount_as_scoped(&server)
1604 .await;
1605
1606 assert_matches!(room0.request_members().await, Ok(()));
1607 }
1608
1609 assert!(room0.are_members_synced());
1611
1612 sliding_sync.set_room_subscriptions(&[room_id_0, room_id_1], None, true);
1613
1614 assert!(room0.are_members_synced().not());
1617
1618 {
1620 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1621
1622 assert!(room_subscriptions.contains_key(room_id_0));
1623 assert!(room_subscriptions.contains_key(room_id_1));
1624 assert!(!room_subscriptions.contains_key(room_id_2));
1625 }
1626
1627 {
1630 struct MemberMatcher(OwnedRoomId);
1631
1632 impl Match for MemberMatcher {
1633 fn matches(&self, request: &Request) -> bool {
1634 request.url.path()
1635 == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1636 && request.method == Method::GET
1637 }
1638 }
1639
1640 let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1641 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1642 "chunk": [],
1643 })))
1644 .mount_as_scoped(&server)
1645 .await;
1646
1647 assert_matches!(room0.request_members().await, Ok(()));
1648 }
1649
1650 assert!(room0.are_members_synced());
1652
1653 sliding_sync.set_room_subscriptions(&[room_id_0], None, false);
1654
1655 assert!(room0.are_members_synced());
1658
1659 {
1661 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1662
1663 assert!(room_subscriptions.contains_key(room_id_0));
1664 assert!(!room_subscriptions.contains_key(room_id_1));
1665 assert!(!room_subscriptions.contains_key(room_id_2));
1666 }
1667 Ok(())
1668 }
1669
1670 #[async_test]
1671 async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1672 let (_server, sliding_sync) = new_sliding_sync(vec![
1673 SlidingSyncList::builder("foo")
1674 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1675 ])
1676 .await?;
1677
1678 let room_id_0 = room_id!("!r0:bar.org");
1679 let room_id_1 = room_id!("!r1:bar.org");
1680 let room_id_2 = room_id!("!r2:bar.org");
1681
1682 sliding_sync.add_room_subscriptions(&[room_id_0, room_id_1], None, false);
1684
1685 {
1686 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1687
1688 assert!(room_subscriptions.contains_key(room_id_0));
1689 assert!(room_subscriptions.contains_key(room_id_1));
1690 assert!(room_subscriptions.contains_key(room_id_2).not());
1691 }
1692
1693 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1695
1696 {
1697 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1698
1699 assert!(room_subscriptions.contains_key(room_id_0));
1700 assert!(room_subscriptions.contains_key(room_id_1));
1701 assert!(room_subscriptions.contains_key(room_id_2));
1702 }
1703
1704 sliding_sync.expire_session().await;
1706
1707 {
1708 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1709
1710 assert!(room_subscriptions.is_empty());
1711 }
1712
1713 sliding_sync.add_room_subscriptions(&[room_id_2], None, false);
1715
1716 {
1717 let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1718
1719 assert!(room_subscriptions.contains_key(room_id_0).not());
1720 assert!(room_subscriptions.contains_key(room_id_1).not());
1721 assert!(room_subscriptions.contains_key(room_id_2));
1722 }
1723
1724 Ok(())
1725 }
1726
1727 #[async_test]
1728 async fn test_add_list() -> Result<()> {
1729 let (_server, sliding_sync) = new_sliding_sync(vec![
1730 SlidingSyncList::builder("foo")
1731 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1732 ])
1733 .await?;
1734
1735 let _stream = sliding_sync.sync();
1736 pin_mut!(_stream);
1737
1738 sliding_sync
1739 .add_list(
1740 SlidingSyncList::builder("bar")
1741 .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1742 )
1743 .await?;
1744
1745 let lists = sliding_sync.inner.lists.read().await;
1746
1747 assert!(lists.contains_key("foo"));
1748 assert!(lists.contains_key("bar"));
1749
1750 Ok(())
1753 }
1754
1755 #[cfg(feature = "e2e-encryption")]
1756 #[async_test]
1757 async fn test_extensions_to_device_since_is_set() {
1758 use matrix_sdk_base::crypto::store::types::Changes;
1759
1760 let client = logged_in_client(None).await;
1761 let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1762 .unwrap()
1763 .with_to_device_extension(assign!(
1764 http::request::ToDevice::default(),
1765 {
1766 enabled: Some(true),
1767 }
1768 ))
1769 .build()
1770 .await
1771 .unwrap();
1772
1773 {
1775 let to_device = &sliding_sync.inner.extensions.to_device;
1776
1777 assert_eq!(to_device.enabled, Some(true));
1778 assert!(to_device.since.is_none());
1779 }
1780
1781 {
1783 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1784
1785 let to_device = &request.extensions.to_device;
1786
1787 assert_eq!(to_device.enabled, Some(true));
1788 assert!(to_device.since.is_none());
1789 }
1790
1791 let since_token = "depuis".to_owned();
1793
1794 {
1795 if let Some(olm_machine) = &*client.olm_machine().await {
1796 olm_machine
1797 .store()
1798 .save_changes(Changes {
1799 next_batch_token: Some(since_token.clone()),
1800 ..Default::default()
1801 })
1802 .await
1803 .unwrap();
1804 } else {
1805 panic!("Where is the Olm machine?");
1806 }
1807 }
1808
1809 {
1811 let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1812
1813 let to_device = &request.extensions.to_device;
1814
1815 assert_eq!(to_device.enabled, Some(true));
1816 assert_eq!(to_device.since, Some(since_token));
1817 }
1818 }
1819
1820 #[async_test]
1826 #[cfg(feature = "e2e-encryption")]
1827 async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
1828 use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
1829 use matrix_sdk_test::ruma_response_from_json;
1830 use ruma::user_id;
1831
1832 let server = MockServer::start().await;
1833 let client = logged_in_client(Some(server.uri())).await;
1834
1835 let alice = user_id!("@alice:localhost");
1836 let bob = user_id!("@bob:localhost");
1837 let me = user_id!("@example:localhost");
1838
1839 {
1842 let olm_machine = client.olm_machine().await;
1843 let olm_machine = olm_machine.as_ref().unwrap();
1844
1845 olm_machine.update_tracked_users([alice, bob]).await?;
1846
1847 let outgoing_requests = olm_machine.outgoing_requests().await?;
1849
1850 assert_eq!(outgoing_requests.len(), 2);
1851 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
1852 assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
1853
1854 olm_machine
1856 .mark_request_as_sent(
1857 outgoing_requests[0].request_id(),
1858 AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
1859 "one_time_key_counts": {}
1860 }))),
1861 )
1862 .await?;
1863
1864 olm_machine
1865 .mark_request_as_sent(
1866 outgoing_requests[1].request_id(),
1867 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1868 "device_keys": {
1869 alice: {},
1870 bob: {},
1871 }
1872 }))),
1873 )
1874 .await?;
1875
1876 let outgoing_requests = olm_machine.outgoing_requests().await?;
1878
1879 assert_eq!(outgoing_requests.len(), 1);
1880 assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
1881
1882 olm_machine
1883 .mark_request_as_sent(
1884 outgoing_requests[0].request_id(),
1885 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1886 "device_keys": {
1887 me: {},
1888 }
1889 }))),
1890 )
1891 .await?;
1892
1893 let outgoing_requests = olm_machine.outgoing_requests().await?;
1895
1896 assert!(outgoing_requests.is_empty());
1897 }
1898
1899 let sync = client
1900 .sliding_sync("test-slidingsync")?
1901 .add_list(SlidingSyncList::builder("new_list"))
1902 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
1903 .build()
1904 .await?;
1905
1906 let (_request, _, _) = sync.generate_sync_request().await?;
1908
1909 {
1911 let olm_machine = client.olm_machine().await;
1912 let olm_machine = olm_machine.as_ref().unwrap();
1913
1914 let outgoing_requests = olm_machine.outgoing_requests().await?;
1916
1917 assert_eq!(outgoing_requests.len(), 1);
1918 assert_matches!(
1919 outgoing_requests[0].request(),
1920 AnyOutgoingRequest::KeysQuery(request) => {
1921 assert!(request.device_keys.contains_key(alice));
1922 assert!(request.device_keys.contains_key(bob));
1923 assert!(request.device_keys.contains_key(me));
1924 }
1925 );
1926
1927 olm_machine
1929 .mark_request_as_sent(
1930 outgoing_requests[0].request_id(),
1931 AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1932 "device_keys": {
1933 alice: {},
1934 bob: {},
1935 me: {},
1936 }
1937 }))),
1938 )
1939 .await?;
1940 }
1941
1942 sync.set_pos("chocolat".to_owned()).await;
1944
1945 let (_request, _, _) = sync.generate_sync_request().await?;
1946
1947 {
1949 let olm_machine = client.olm_machine().await;
1950 let olm_machine = olm_machine.as_ref().unwrap();
1951
1952 let outgoing_requests = olm_machine.outgoing_requests().await?;
1954
1955 assert!(outgoing_requests.is_empty());
1956 }
1957
1958 Ok(())
1959 }
1960
1961 #[cfg(feature = "e2e-encryption")]
1962 #[async_test]
1963 async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
1964 let server = MockServer::start().await;
1965
1966 #[derive(Deserialize)]
1967 struct PartialRequest {
1968 txn_id: Option<String>,
1969 }
1970
1971 let server_pos = Arc::new(Mutex::new(0));
1972 let _mock_guard = Mock::given(SlidingSyncMatcher)
1973 .respond_with(move |request: &Request| {
1974 let request: PartialRequest = request.body_json().unwrap();
1976 let pos = {
1977 let mut pos = server_pos.lock().unwrap();
1978 let prev = *pos;
1979 *pos += 1;
1980 prev
1981 };
1982
1983 ResponseTemplate::new(200).set_body_json(json!({
1984 "txn_id": request.txn_id,
1985 "pos": pos.to_string(),
1986 }))
1987 })
1988 .mount_as_scoped(&server)
1989 .await;
1990
1991 let client = logged_in_client(Some(server.uri())).await;
1992
1993 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1994
1995 {
1997 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1998
1999 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2000 assert!(request.pos.is_none());
2001 }
2002
2003 let sync = sliding_sync.sync();
2004 pin_mut!(sync);
2005
2006 let next = sync.next().await;
2009 assert_matches!(next, Some(Ok(_update_summary)));
2010
2011 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2012
2013 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2014 .await?
2015 .expect("must have restored fields");
2016
2017 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2020
2021 {
2025 let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2026
2027 let mut position_guard = other_sync.inner.position.lock().await;
2028 position_guard.pos = Some("yolo".to_owned());
2029
2030 other_sync.cache_to_storage(&position_guard).await?;
2031 }
2032
2033 {
2035 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
2036 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2037 assert_eq!(request.pos.as_deref(), Some("0"));
2038 }
2039
2040 {
2043 let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
2044 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2045 }
2046
2047 Ok(())
2048 }
2049
2050 #[cfg(feature = "e2e-encryption")]
2051 #[async_test]
2052 async fn test_sliding_sync_does_remember_pos() -> Result<()> {
2053 let server = MockServer::start().await;
2054
2055 #[derive(Deserialize)]
2056 struct PartialRequest {
2057 txn_id: Option<String>,
2058 }
2059
2060 let server_pos = Arc::new(Mutex::new(0));
2061 let _mock_guard = Mock::given(SlidingSyncMatcher)
2062 .respond_with(move |request: &Request| {
2063 let request: PartialRequest = request.body_json().unwrap();
2065 let pos = {
2066 let mut pos = server_pos.lock().unwrap();
2067 let prev = *pos;
2068 *pos += 1;
2069 prev
2070 };
2071
2072 ResponseTemplate::new(200).set_body_json(json!({
2073 "txn_id": request.txn_id,
2074 "pos": pos.to_string(),
2075 }))
2076 })
2077 .mount_as_scoped(&server)
2078 .await;
2079
2080 let client = logged_in_client(Some(server.uri())).await;
2081
2082 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2083
2084 {
2086 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2087
2088 assert!(request.pos.is_none());
2089 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2090 }
2091
2092 let sync = sliding_sync.sync();
2093 pin_mut!(sync);
2094
2095 let next = sync.next().await;
2098 assert_matches!(next, Some(Ok(_update_summary)));
2099
2100 assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
2101
2102 let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
2103 .await?
2104 .expect("must have restored fields");
2105
2106 assert_eq!(restored_fields.pos.as_deref(), Some("0"));
2109
2110 {
2112 let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
2113
2114 let mut position_guard = other_sync.inner.position.lock().await;
2115 position_guard.pos = Some("42".to_owned());
2116
2117 other_sync.cache_to_storage(&position_guard).await?;
2118 }
2119
2120 {
2122 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2123 assert_eq!(request.pos.as_deref(), Some("42"));
2124 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2125 }
2126
2127 {
2129 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2130 assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
2131
2132 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2133 assert_eq!(request.pos.as_deref(), Some("42"));
2134 }
2135
2136 sliding_sync.expire_session().await;
2139
2140 {
2141 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2142
2143 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2144 assert!(request.pos.is_none());
2145 }
2146
2147 {
2149 let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
2150 assert!(sliding_sync.inner.position.lock().await.pos.is_none());
2151
2152 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2153 assert!(request.pos.is_none());
2154 }
2155
2156 Ok(())
2157 }
2158
2159 #[async_test]
2160 async fn test_stop_sync_loop() -> Result<()> {
2161 let (_server, sliding_sync) = new_sliding_sync(vec![
2162 SlidingSyncList::builder("foo")
2163 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2164 ])
2165 .await?;
2166
2167 let stream = sliding_sync.sync();
2169 pin_mut!(stream);
2170
2171 assert!(stream.next().await.is_some());
2173
2174 sliding_sync.stop_sync()?;
2176
2177 assert!(stream.next().await.is_none());
2179
2180 let stream = sliding_sync.sync();
2182 pin_mut!(stream);
2183
2184 assert!(stream.next().await.is_some());
2186
2187 Ok(())
2188 }
2189
2190 #[async_test]
2191 async fn test_process_read_receipts() -> Result<()> {
2192 let room = owned_room_id!("!pony:example.org");
2193
2194 let server = MockServer::start().await;
2195 let client = logged_in_client(Some(server.uri())).await;
2196 client.event_cache().subscribe().unwrap();
2197
2198 let sliding_sync = client
2199 .sliding_sync("test")?
2200 .with_receipt_extension(
2201 assign!(http::request::Receipts::default(), { enabled: Some(true) }),
2202 )
2203 .add_list(
2204 SlidingSyncList::builder("all")
2205 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2206 )
2207 .build()
2208 .await?;
2209
2210 {
2212 let server_response = assign!(http::Response::new("0".to_owned()), {
2213 rooms: BTreeMap::from([(
2214 room.clone(),
2215 http::response::Room::default(),
2216 )])
2217 });
2218
2219 let _summary = {
2220 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2221 sliding_sync
2222 .handle_response(
2223 server_response.clone(),
2224 &mut pos_guard,
2225 RequestedRequiredStates::default(),
2226 )
2227 .await?
2228 };
2229 }
2230
2231 let server_response = assign!(http::Response::new("1".to_owned()), {
2232 extensions: assign!(http::response::Extensions::default(), {
2233 receipts: assign!(http::response::Receipts::default(), {
2234 rooms: BTreeMap::from([
2235 (
2236 room.clone(),
2237 Raw::from_json_string(
2238 json!({
2239 "room_id": room,
2240 "type": "m.receipt",
2241 "content": {
2242 "$event:bar.org": {
2243 "m.read": {
2244 client.user_id().unwrap(): {
2245 "ts": 1436451550,
2246 }
2247 }
2248 }
2249 }
2250 })
2251 .to_string(),
2252 ).unwrap()
2253 )
2254 ])
2255 })
2256 })
2257 });
2258
2259 let summary = {
2260 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2261 sliding_sync
2262 .handle_response(
2263 server_response.clone(),
2264 &mut pos_guard,
2265 RequestedRequiredStates::default(),
2266 )
2267 .await?
2268 };
2269
2270 assert!(summary.rooms.contains(&room));
2271
2272 Ok(())
2273 }
2274
2275 #[async_test]
2276 async fn test_process_marked_unread_room_account_data() -> Result<()> {
2277 let room_id = owned_room_id!("!unicorn:example.org");
2278
2279 let server = MockServer::start().await;
2280 let client = logged_in_client(Some(server.uri())).await;
2281
2282 let sliding_sync = client
2285 .sliding_sync("test")?
2286 .with_account_data_extension(
2287 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2288 )
2289 .add_list(
2290 SlidingSyncList::builder("all")
2291 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2292 )
2293 .build()
2294 .await?;
2295
2296 {
2298 let server_response = assign!(http::Response::new("0".to_owned()), {
2299 rooms: BTreeMap::from([(
2300 room_id.clone(),
2301 http::response::Room::default(),
2302 )])
2303 });
2304
2305 let _summary = {
2306 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2307 sliding_sync
2308 .handle_response(
2309 server_response.clone(),
2310 &mut pos_guard,
2311 RequestedRequiredStates::default(),
2312 )
2313 .await?
2314 };
2315 }
2316
2317 let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2321
2322 let update_summary = {
2323 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2324 sliding_sync
2325 .handle_response(
2326 server_response.clone(),
2327 &mut pos_guard,
2328 RequestedRequiredStates::default(),
2329 )
2330 .await?
2331 };
2332
2333 assert!(update_summary.rooms.contains(&room_id));
2336
2337 let room = client.get_room(&room_id).unwrap();
2338
2339 assert!(room.is_marked_unread());
2342
2343 let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2346
2347 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2348 sliding_sync
2349 .handle_response(
2350 server_response.clone(),
2351 &mut pos_guard,
2352 RequestedRequiredStates::default(),
2353 )
2354 .await?;
2355
2356 let room = client.get_room(&room_id).unwrap();
2357
2358 assert!(!room.is_marked_unread());
2359
2360 Ok(())
2361 }
2362
2363 fn make_mark_unread_response(
2364 response_number: &str,
2365 room_id: OwnedRoomId,
2366 unread: bool,
2367 add_rooms_section: bool,
2368 ) -> http::Response {
2369 let rooms = if add_rooms_section {
2370 BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2371 } else {
2372 BTreeMap::new()
2373 };
2374
2375 let extensions = assign!(http::response::Extensions::default(), {
2376 account_data: assign!(http::response::AccountData::default(), {
2377 rooms: BTreeMap::from([
2378 (
2379 room_id,
2380 vec![
2381 Raw::from_json_string(
2382 json!({
2383 "content": {
2384 "unread": unread
2385 },
2386 "type": "m.marked_unread"
2387 })
2388 .to_string(),
2389 ).unwrap()
2390 ]
2391 )
2392 ])
2393 })
2394 });
2395
2396 assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2397 }
2398
2399 #[async_test]
2400 async fn test_process_rooms_account_data() -> Result<()> {
2401 let room = owned_room_id!("!pony:example.org");
2402
2403 let server = MockServer::start().await;
2404 let client = logged_in_client(Some(server.uri())).await;
2405
2406 let sliding_sync = client
2407 .sliding_sync("test")?
2408 .with_account_data_extension(
2409 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2410 )
2411 .add_list(
2412 SlidingSyncList::builder("all")
2413 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2414 )
2415 .build()
2416 .await?;
2417
2418 {
2420 let server_response = assign!(http::Response::new("0".to_owned()), {
2421 rooms: BTreeMap::from([(
2422 room.clone(),
2423 http::response::Room::default(),
2424 )])
2425 });
2426
2427 let _summary = {
2428 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2429 sliding_sync
2430 .handle_response(
2431 server_response.clone(),
2432 &mut pos_guard,
2433 RequestedRequiredStates::default(),
2434 )
2435 .await?
2436 };
2437 }
2438
2439 let server_response = assign!(http::Response::new("1".to_owned()), {
2440 extensions: assign!(http::response::Extensions::default(), {
2441 account_data: assign!(http::response::AccountData::default(), {
2442 rooms: BTreeMap::from([
2443 (
2444 room.clone(),
2445 vec![
2446 Raw::from_json_string(
2447 json!({
2448 "content": {
2449 "tags": {
2450 "u.work": {
2451 "order": 0.9
2452 }
2453 }
2454 },
2455 "type": "m.tag"
2456 })
2457 .to_string(),
2458 ).unwrap()
2459 ]
2460 )
2461 ])
2462 })
2463 })
2464 });
2465 let 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!(summary.rooms.contains(&room));
2477
2478 Ok(())
2479 }
2480
2481 #[async_test]
2482 #[cfg(feature = "e2e-encryption")]
2483 async fn test_process_only_encryption_events() -> Result<()> {
2484 use ruma::OneTimeKeyAlgorithm;
2485
2486 let room = owned_room_id!("!croissant:example.org");
2487
2488 let server = MockServer::start().await;
2489 let client = logged_in_client(Some(server.uri())).await;
2490
2491 let server_response = assign!(http::Response::new("0".to_owned()), {
2492 rooms: BTreeMap::from([(
2493 room.clone(),
2494 assign!(http::response::Room::default(), {
2495 name: Some("Croissants lovers".to_owned()),
2496 timeline: Vec::new(),
2497 }),
2498 )]),
2499
2500 extensions: assign!(http::response::Extensions::default(), {
2501 e2ee: assign!(http::response::E2EE::default(), {
2502 device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2503 }),
2504 to_device: Some(assign!(http::response::ToDevice::default(), {
2505 next_batch: "to-device-token".to_owned(),
2506 })),
2507 })
2508 });
2509
2510 let sliding_sync = client
2514 .sliding_sync("test")?
2515 .with_to_device_extension(
2516 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2517 )
2518 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2519 .build()
2520 .await?;
2521
2522 {
2523 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2524
2525 sliding_sync
2526 .handle_response(
2527 server_response.clone(),
2528 &mut position_guard,
2529 RequestedRequiredStates::default(),
2530 )
2531 .await?;
2532 }
2533
2534 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2536 assert_eq!(uploaded_key_count, 42);
2537
2538 {
2539 let olm_machine = &*client.olm_machine_for_testing().await;
2540 assert_eq!(
2541 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2542 Some("to-device-token")
2543 );
2544 }
2545
2546 assert!(client.get_room(&room).is_none());
2548
2549 let client = logged_in_client(Some(server.uri())).await;
2552
2553 let sliding_sync = client
2554 .sliding_sync("test")?
2555 .add_list(SlidingSyncList::builder("thelist"))
2556 .build()
2557 .await?;
2558
2559 {
2560 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2561
2562 sliding_sync
2563 .handle_response(
2564 server_response.clone(),
2565 &mut position_guard,
2566 RequestedRequiredStates::default(),
2567 )
2568 .await?;
2569 }
2570
2571 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2573 assert_eq!(uploaded_key_count, 0);
2574
2575 {
2576 let olm_machine = &*client.olm_machine_for_testing().await;
2577 assert_eq!(
2578 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2579 None
2580 );
2581 }
2582
2583 assert!(client.get_room(&room).is_some());
2585
2586 let client = logged_in_client(Some(server.uri())).await;
2588
2589 let sliding_sync = client
2590 .sliding_sync("test")?
2591 .add_list(SlidingSyncList::builder("thelist"))
2592 .with_to_device_extension(
2593 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2594 )
2595 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2596 .build()
2597 .await?;
2598
2599 {
2600 let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2601
2602 sliding_sync
2603 .handle_response(
2604 server_response.clone(),
2605 &mut position_guard,
2606 RequestedRequiredStates::default(),
2607 )
2608 .await?;
2609 }
2610
2611 let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2613 assert_eq!(uploaded_key_count, 42);
2614
2615 {
2616 let olm_machine = &*client.olm_machine_for_testing().await;
2617 assert_eq!(
2618 olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2619 Some("to-device-token")
2620 );
2621 }
2622
2623 assert!(client.get_room(&room).is_some());
2625
2626 Ok(())
2627 }
2628
2629 #[async_test]
2630 async fn test_lock_multiple_requests() -> Result<()> {
2631 let server = MockServer::start().await;
2632 let client = logged_in_client(Some(server.uri())).await;
2633
2634 let pos = Arc::new(Mutex::new(0));
2635 let _mock_guard = Mock::given(SlidingSyncMatcher)
2636 .respond_with(move |_: &Request| {
2637 let mut pos = pos.lock().unwrap();
2638 *pos += 1;
2639 ResponseTemplate::new(200).set_body_json(json!({
2640 "pos": pos.to_string(),
2641 "lists": {},
2642 "rooms": {}
2643 }))
2644 })
2645 .mount_as_scoped(&server)
2646 .await;
2647
2648 let sliding_sync = client
2649 .sliding_sync("test")?
2650 .with_to_device_extension(
2651 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2652 )
2653 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2654 .build()
2655 .await?;
2656
2657 let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2660
2661 for result in requests.await {
2662 result?;
2663 }
2664
2665 Ok(())
2666 }
2667
2668 #[async_test]
2669 async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2670 let server = MockServer::start().await;
2671 let client = logged_in_client(Some(server.uri())).await;
2672
2673 let pos = Arc::new(Mutex::new(0));
2674 let _mock_guard = Mock::given(SlidingSyncMatcher)
2675 .respond_with(move |_: &Request| {
2676 let mut pos = pos.lock().unwrap();
2677 *pos += 1;
2678 ResponseTemplate::new(200)
2680 .set_body_json(json!({
2681 "pos": pos.to_string(),
2682 "lists": {},
2683 "rooms": {}
2684 }))
2685 .set_delay(Duration::from_secs(2))
2686 })
2687 .mount_as_scoped(&server)
2688 .await;
2689
2690 let sliding_sync =
2691 client
2692 .sliding_sync("test")?
2693 .add_list(SlidingSyncList::builder("room-list").sync_mode(
2694 SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2695 ))
2696 .add_list(
2697 SlidingSyncList::builder("another-list")
2698 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2699 )
2700 .build()
2701 .await?;
2702
2703 let stream = sliding_sync.sync();
2704 pin_mut!(stream);
2705
2706 let cloned_sync = sliding_sync.clone();
2707 spawn(async move {
2708 tokio::time::sleep(Duration::from_millis(100)).await;
2709
2710 cloned_sync
2711 .on_list("another-list", |list| {
2712 list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2713 ready(())
2714 })
2715 .await;
2716 });
2717
2718 assert_matches!(stream.next().await, Some(Ok(_)));
2719
2720 sliding_sync.stop_sync().unwrap();
2721
2722 assert_matches!(stream.next().await, None);
2723
2724 let mut num_requests = 0;
2725
2726 for request in server.received_requests().await.unwrap() {
2727 if !SlidingSyncMatcher.matches(&request) {
2728 continue;
2729 }
2730
2731 let another_list_ranges = if num_requests == 0 {
2732 json!([[0, 10]])
2734 } else {
2735 json!([[10, 20]])
2737 };
2738
2739 num_requests += 1;
2740 assert!(num_requests <= 2, "more than one request hit the server");
2741
2742 let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2743
2744 if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2745 &json_value,
2746 &json!({
2747 "conn_id": "test",
2748 "lists": {
2749 "room-list": {
2750 "ranges": [[0, 9]],
2751 "required_state": [
2752 ["m.room.encryption", ""],
2753 ["m.room.tombstone", ""]
2754 ],
2755 },
2756 "another-list": {
2757 "ranges": another_list_ranges,
2758 "required_state": [
2759 ["m.room.encryption", ""],
2760 ["m.room.tombstone", ""]
2761 ],
2762 },
2763 }
2764 }),
2765 assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2766 ) {
2767 dbg!(json_value);
2768 panic!("json differ: {err}");
2769 }
2770 }
2771
2772 assert_eq!(num_requests, 2);
2773
2774 Ok(())
2775 }
2776
2777 #[async_test]
2778 async fn test_timeout_zero_list() -> Result<()> {
2779 let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2780
2781 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2782
2783 assert!(request.timeout.is_some());
2786
2787 Ok(())
2788 }
2789
2790 #[async_test]
2791 async fn test_timeout_one_list() -> Result<()> {
2792 let (_server, sliding_sync) = new_sliding_sync(vec![
2793 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2794 ])
2795 .await?;
2796
2797 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2798
2799 assert!(request.timeout.is_none());
2801
2802 {
2804 let server_response = assign!(http::Response::new("0".to_owned()), {
2805 lists: BTreeMap::from([(
2806 "foo".to_owned(),
2807 assign!(http::response::List::default(), {
2808 count: uint!(7),
2809 })
2810 )])
2811 });
2812
2813 let _summary = {
2814 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2815 sliding_sync
2816 .handle_response(
2817 server_response.clone(),
2818 &mut pos_guard,
2819 RequestedRequiredStates::default(),
2820 )
2821 .await?
2822 };
2823 }
2824
2825 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2826
2827 assert!(request.timeout.is_some());
2829
2830 Ok(())
2831 }
2832
2833 #[async_test]
2834 async fn test_timeout_three_lists() -> Result<()> {
2835 let (_server, sliding_sync) = new_sliding_sync(vec![
2836 SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2837 SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
2838 SlidingSyncList::builder("baz")
2839 .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2840 ])
2841 .await?;
2842
2843 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2844
2845 assert!(request.timeout.is_none());
2847
2848 {
2850 let server_response = assign!(http::Response::new("0".to_owned()), {
2851 lists: BTreeMap::from([(
2852 "foo".to_owned(),
2853 assign!(http::response::List::default(), {
2854 count: uint!(7),
2855 })
2856 )])
2857 });
2858
2859 let _summary = {
2860 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2861 sliding_sync
2862 .handle_response(
2863 server_response.clone(),
2864 &mut pos_guard,
2865 RequestedRequiredStates::default(),
2866 )
2867 .await?
2868 };
2869 }
2870
2871 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2872
2873 assert!(request.timeout.is_none());
2875
2876 {
2878 let server_response = assign!(http::Response::new("1".to_owned()), {
2879 lists: BTreeMap::from([(
2880 "bar".to_owned(),
2881 assign!(http::response::List::default(), {
2882 count: uint!(7),
2883 })
2884 )])
2885 });
2886
2887 let _summary = {
2888 let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2889 sliding_sync
2890 .handle_response(
2891 server_response.clone(),
2892 &mut pos_guard,
2893 RequestedRequiredStates::default(),
2894 )
2895 .await?
2896 };
2897 }
2898
2899 let (request, _, _) = sliding_sync.generate_sync_request().await?;
2900
2901 assert!(request.timeout.is_some());
2903
2904 Ok(())
2905 }
2906
2907 #[async_test]
2908 async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
2909 let server = MockServer::start().await;
2910 let client = logged_in_client(Some(server.uri())).await;
2911
2912 let _mock_guard = Mock::given(SlidingSyncMatcher)
2913 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2914 "pos": "0",
2915 "lists": {},
2916 "rooms": {}
2917 })))
2918 .mount_as_scoped(&server)
2919 .await;
2920
2921 let sliding_sync = client
2922 .sliding_sync("test")?
2923 .with_to_device_extension(
2924 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2925 )
2926 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2927 .build()
2928 .await?;
2929
2930 let sliding_sync = Arc::new(sliding_sync);
2931
2932 let sync_beat_listener = client.inner.sync_beat.listen();
2934 sliding_sync.sync_once().await?;
2935
2936 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
2938 Ok(())
2939 }
2940
2941 #[async_test]
2942 async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
2943 let server = MockServer::start().await;
2944 let client = logged_in_client(Some(server.uri())).await;
2945
2946 let _mock_guard = Mock::given(SlidingSyncMatcher)
2947 .respond_with(ResponseTemplate::new(404))
2948 .mount_as_scoped(&server)
2949 .await;
2950
2951 let sliding_sync = client
2952 .sliding_sync("test")?
2953 .with_to_device_extension(
2954 assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2955 )
2956 .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2957 .build()
2958 .await?;
2959
2960 let sliding_sync = Arc::new(sliding_sync);
2961
2962 let sync_beat_listener = client.inner.sync_beat.listen();
2964 let sync_result = sliding_sync.sync_once().await;
2965 assert!(sync_result.is_err());
2966
2967 assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
2969
2970 Ok(())
2971 }
2972
2973 #[async_test]
2974 async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
2975 let server = MatrixMockServer::new().await;
2976 let client = server.client_builder().build().await;
2977 let room_id = room_id!("!mu5hr00m:example.org");
2978
2979 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2980 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2981 "pos": "0",
2982 "lists": {},
2983 "extensions": {
2984 "account_data": {
2985 "global": [
2986 {
2987 "type": "m.direct",
2988 "content": {
2989 "@de4dlockh0lmes:example.org": [
2990 "!mu5hr00m:example.org"
2991 ]
2992 }
2993 }
2994 ]
2995 }
2996 },
2997 "rooms": {
2998 room_id: {
2999 "name": "Mario Bros Fanbase Room",
3000 "initial": true,
3001 },
3002 }
3003 })))
3004 .mount_as_scoped(server.server())
3005 .await;
3006
3007 let f = EventFactory::new().room(room_id);
3008
3009 Mock::given(method("GET"))
3010 .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
3011 .respond_with(ResponseTemplate::new(200).set_body_json(json!({
3012 "chunk": [
3013 f.member(&ALICE).membership(MembershipState::Join).into_raw_timeline(),
3014 ]
3015 })))
3016 .mount(server.server())
3017 .await;
3018
3019 let (tx, rx) = tokio::sync::oneshot::channel();
3020
3021 let tx = Arc::new(Mutex::new(Some(tx)));
3022 client.add_event_handler(move |_: DirectEvent, client: Client| async move {
3023 let members =
3025 client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
3026 assert_eq!(members.len(), 1);
3027 tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
3028 });
3029
3030 let sliding_sync = client
3031 .sliding_sync("test")?
3032 .add_list(SlidingSyncList::builder("thelist"))
3033 .with_account_data_extension(
3034 assign!(http::request::AccountData::default(), { enabled: Some(true) }),
3035 )
3036 .build()
3037 .await?;
3038
3039 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3040 .await
3041 .expect("Sync did not complete in time")
3042 .expect("Sync failed");
3043
3044 tokio::time::timeout(Duration::from_secs(5), rx)
3046 .await
3047 .expect("Event handler did not complete in time")
3048 .expect("Event handler failed");
3049
3050 Ok(())
3051 }
3052
3053 #[cfg(feature = "e2e-encryption")]
3054 #[async_test]
3055 async fn test_syncing_one_time_key_counts_updates() -> Result<()> {
3056 macro_rules! assert_key_count {
3057 ($client: ident, $count:literal) => {{
3058 let machine = $client.olm_machine().await;
3059 let uploaded_key_counts =
3060 machine.as_ref().unwrap().uploaded_key_count().await.unwrap();
3061 assert_eq!(uploaded_key_counts, $count)
3062 }};
3063 }
3064
3065 macro_rules! sync_with_key_count {
3066 ($client: ident, $server:ident, $count:literal) => {
3067 let count = Some($count);
3068 sync_with_key_count!($client, $server, count);
3069 };
3070 ($client: ident, $server:ident, $count:ident) => {{
3071 let count: Option<u32> = $count;
3072
3073 let template = if let Some(count) = count {
3074 ResponseTemplate::new(200).set_body_json(json!({
3075 "pos": "0",
3076 "lists": {},
3077 "extensions": {
3078 "e2ee": {
3079 "device_one_time_keys_count": {
3080 "signed_curve25519": count,
3081 }
3082 }
3083 },
3084 }))
3085 } else {
3086 ResponseTemplate::new(200).set_body_json(json!({
3087 "pos": "0",
3088 "lists": {},
3089 "extensions": {
3090 "e2ee": {}
3091 },
3092 }))
3093 };
3094
3095 let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
3096 .respond_with(template)
3097 .mount_as_scoped($server.server())
3098 .await;
3099
3100 let sliding_sync = $client
3101 .sliding_sync("test")?
3102 .with_e2ee_extension(
3103 assign!(http::request::E2EE::default(), { enabled: Some(true)}),
3104 )
3105 .build()
3106 .await?;
3107
3108 tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
3109 .await
3110 .expect("Sync did not complete in time")
3111 .expect("Sync failed");
3112 }}
3113 }
3114
3115 let server = MatrixMockServer::new().await;
3116 let client = server.client_builder().build().await;
3117
3118 server.mock_upload_keys().ok_with_signed_curve_key_count(50).mock_once().mount().await;
3119
3120 assert_key_count!(client, 0);
3122
3123 sync_with_key_count!(client, server, None);
3125 assert_key_count!(client, 50);
3126
3127 sync_with_key_count!(client, server, None);
3129 assert_key_count!(client, 50);
3130
3131 sync_with_key_count!(client, server, 10);
3133 assert_key_count!(client, 10);
3134
3135 Ok(())
3136 }
3137}