Skip to main content

matrix_sdk/sliding_sync/
mod.rs

1// Copyright 2022-2023 Benjamin Kampmann
2// Copyright 2022 The Matrix.org Foundation C.I.C.
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for that specific language governing permissions and
14// limitations under the License.
15
16#![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, RwLockWriteGuard as StdRwLockWriteGuard},
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/// The Sliding Sync instance.
55///
56/// It is OK to clone this type as much as you need: cloning it is cheap.
57#[derive(Clone, Debug)]
58pub struct SlidingSync {
59    /// The Sliding Sync data.
60    inner: Arc<SlidingSyncInner>,
61}
62
63#[derive(Debug)]
64pub(super) struct SlidingSyncInner {
65    /// A unique identifier for this instance of sliding sync.
66    ///
67    /// Used to distinguish different connections to sliding sync.
68    id: String,
69
70    /// The HTTP Matrix client.
71    client: Client,
72
73    /// Long-polling timeout that appears in sliding sync request.
74    poll_timeout: Duration,
75
76    /// Extra duration for the sliding sync request to timeout. This is added to
77    /// the [`Self::poll_timeout`].
78    network_timeout: Duration,
79
80    /// The storage key to keep this cache at and load it from.
81    storage_key: String,
82
83    /// Should this sliding sync instance try to restore its sync position
84    /// from the database?
85    ///
86    /// Note: in non-cfg(e2e-encryption) builds, it's always set to false. We
87    /// keep it even so, to avoid sparkling cfg statements everywhere
88    /// throughout this file.
89    share_pos: bool,
90
91    /// Position markers.
92    ///
93    /// The `pos` marker represents a progression when exchanging requests and
94    /// responses with the server: the server acknowledges the request by
95    /// responding with a new `pos`. If the client sends two non-necessarily
96    /// consecutive requests with the same `pos`, the server has to reply with
97    /// the same identical response.
98    ///
99    /// `position` is behind a mutex so that a new request starts after the
100    /// previous request trip has fully ended (successfully or not). This
101    /// mechanism exists to wait for the response to be handled and to see the
102    /// `position` being updated, before sending a new request.
103    position: Arc<AsyncMutex<SlidingSyncPositionMarkers>>,
104
105    /// The lists of this Sliding Sync instance.
106    lists: AsyncRwLock<BTreeMap<String, SlidingSyncList>>,
107
108    /// Room subscriptions, i.e. rooms that may be out-of-scope of all lists
109    /// but one wants to receive updates.
110    room_subscriptions: StdRwLock<BTreeMap<OwnedRoomId, http::request::RoomSubscription>>,
111
112    /// The intended state of the extensions being supplied to sliding /sync
113    /// calls.
114    extensions: http::request::Extensions,
115
116    /// Internal channel used to pass messages between Sliding Sync and other
117    /// types.
118    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    /// Create a new [`SlidingSyncBuilder`].
131    pub fn builder(id: String, client: Client) -> Result<SlidingSyncBuilder, Error> {
132        SlidingSyncBuilder::new(id, client)
133    }
134
135    /// Add subscriptions to many rooms.
136    ///
137    /// If the associated `Room`s exist, they will be marked as members are
138    /// missing, so that it ensures to re-fetch all members.
139    ///
140    /// A subscription to an already subscribed room is ignored.
141    pub fn subscribe_to_rooms(
142        &self,
143        room_ids: &[&RoomId],
144        settings: Option<http::request::RoomSubscription>,
145        cancel_in_flight_request: bool,
146    ) {
147        if subscribe_to_rooms(
148            self.inner.room_subscriptions.write().unwrap(),
149            &self.inner.client,
150            room_ids,
151            settings,
152            cancel_in_flight_request,
153        ) {
154            self.inner.internal_channel_send_if_possible(
155                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
156            );
157        }
158    }
159
160    /// Remove subscriptions to many rooms.
161    pub fn unsubscribe_to_rooms(&self, room_ids: &[&RoomId], cancel_in_flight_request: bool) {
162        let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
163        let mut skip_over_current_sync_loop_iteration = false;
164
165        for room_id in room_ids {
166            if room_subscriptions.remove(*room_id).is_some() {
167                skip_over_current_sync_loop_iteration = true;
168            }
169        }
170
171        if cancel_in_flight_request && skip_over_current_sync_loop_iteration {
172            self.inner.internal_channel_send_if_possible(
173                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
174            );
175        }
176    }
177
178    /// Add subscriptions to the specified rooms if they don't already exist and
179    /// remove any existing subscriptions to other rooms.
180    ///
181    /// This is similar to [`Self::clear_and_subscribe_to_rooms`] but doesn't
182    /// clear and then recreate all subscriptions. Instead, it will perform
183    /// a delta-like update which involves:
184    ///
185    /// - leaving existing subscriptions untouched if the room is contained  in
186    ///   `room_ids`
187    /// - adding new subscriptions for rooms in `room_ids` that are currently
188    ///   unsubscribed
189    /// - removing existing subscriptions for rooms that are not contained in
190    ///   `room_ids`
191    ///
192    /// Note that unlike [`Self::clear_and_subscribe_to_rooms`], this method
193    /// will not mark members as unsynced (which would cause them to be
194    /// refetched) for subscriptions that already exist.
195    pub fn resubscribe_to_rooms(
196        &self,
197        room_ids: &[&RoomId],
198        settings: Option<http::request::RoomSubscription>,
199        cancel_in_flight_request: bool,
200    ) {
201        let mut room_subscriptions = self.inner.room_subscriptions.write().unwrap();
202        let mut skip_over_current_sync_loop_iteration = false;
203
204        // Remove rooms that are no longer subscribed.
205        room_subscriptions.retain(|room_id, _| {
206            if room_ids.contains(&room_id.as_ref()) {
207                true
208            } else {
209                skip_over_current_sync_loop_iteration = true;
210                false
211            }
212        });
213
214        if subscribe_to_rooms(
215            room_subscriptions,
216            &self.inner.client,
217            room_ids,
218            settings,
219            cancel_in_flight_request && skip_over_current_sync_loop_iteration,
220        ) {
221            self.inner.internal_channel_send_if_possible(
222                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
223            );
224        }
225    }
226
227    /// Replace all subscriptions to rooms by other ones.
228    ///
229    /// If the associated `Room`s exist, they will be marked as members are
230    /// missing, so that it ensures to re-fetch all members.
231    pub fn clear_and_subscribe_to_rooms(
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        room_subscriptions.clear();
239
240        if subscribe_to_rooms(
241            room_subscriptions,
242            &self.inner.client,
243            room_ids,
244            settings,
245            cancel_in_flight_request,
246        ) {
247            self.inner.internal_channel_send_if_possible(
248                SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
249            );
250        }
251    }
252
253    /// Find a list by its name, and do something on it if it exists.
254    pub async fn on_list<Function, FunctionOutput, R>(
255        &self,
256        list_name: &str,
257        function: Function,
258    ) -> Option<R>
259    where
260        Function: FnOnce(&SlidingSyncList) -> FunctionOutput,
261        FunctionOutput: Future<Output = R>,
262    {
263        let lists = self.inner.lists.read().await;
264
265        match lists.get(list_name) {
266            Some(list) => Some(function(list).await),
267            None => None,
268        }
269    }
270
271    /// Add the list to the list of lists.
272    ///
273    /// As lists need to have a unique `.name`, if a list with the same name
274    /// is found the new list will replace the old one and the return it or
275    /// `None`.
276    pub async fn add_list(
277        &self,
278        list_builder: SlidingSyncListBuilder,
279    ) -> Result<Option<SlidingSyncList>> {
280        let list = list_builder.build(self.inner.internal_channel.clone());
281
282        let old_list = self.inner.lists.write().await.insert(list.name().to_owned(), list);
283
284        self.inner.internal_channel_send_if_possible(
285            SlidingSyncInternalMessage::SyncLoopSkipOverCurrentIteration,
286        );
287
288        Ok(old_list)
289    }
290
291    /// Add a list that will be cached and reloaded from the cache.
292    ///
293    /// This will raise an error if a storage key was not set, or if there
294    /// was a I/O error reading from the cache.
295    ///
296    /// The rest of the semantics is the same as [`Self::add_list`].
297    pub async fn add_cached_list(
298        &self,
299        mut list_builder: SlidingSyncListBuilder,
300    ) -> Result<Option<SlidingSyncList>> {
301        let _timer = timer!(format!("restoring (loading+processing) list {}", list_builder.name));
302
303        list_builder.set_cached_and_reload(&self.inner.client, &self.inner.storage_key).await?;
304
305        self.add_list(list_builder).await
306    }
307
308    /// Handle the HTTP response.
309    #[instrument(skip_all)]
310    async fn handle_response(
311        &self,
312        mut sliding_sync_response: http::Response,
313        position: &mut SlidingSyncPositionMarkers,
314        requested_required_states: RequestedRequiredStates,
315    ) -> Result<UpdateSummary, crate::Error> {
316        let pos = Some(sliding_sync_response.pos.clone());
317
318        let must_process_rooms_response = self.must_process_rooms_response().await;
319
320        trace!(yes = must_process_rooms_response, "Must process rooms response?");
321
322        // Transform a Sliding Sync Response to a `SyncResponse`.
323        //
324        // We may not need the `sync_response` in the future (once `SyncResponse` will
325        // move to Sliding Sync, i.e. to `http::Response`), but processing the
326        // `sliding_sync_response` is vital, so it must be done somewhere; for now it
327        // happens here.
328
329        let sync_response = {
330            let _timer = timer!("response processor");
331
332            let response_processor = {
333                // Take the lock to synchronise accesses to the state store, to avoid concurrent
334                // sliding syncs overwriting each other's room infos.
335                let state_store_guard = {
336                    let _timer = timer!("acquiring the `state_store_lock`");
337
338                    self.inner.client.base_client().state_store_lock().lock().await
339                };
340
341                let mut response_processor =
342                    SlidingSyncResponseProcessor::new(self.inner.client.clone());
343
344                // Process thread subscriptions if they're available.
345                //
346                // It's important to do this *before* handling the room responses, so that
347                // notifications can be properly generated based on the thread subscriptions,
348                // for the events in threads we've subscribed to.
349                if self.is_thread_subscriptions_enabled() {
350                    response_processor
351                        .handle_thread_subscriptions(
352                            position.pos.as_deref(),
353                            std::mem::take(
354                                &mut sliding_sync_response.extensions.thread_subscriptions,
355                            ),
356                        )
357                        .await?;
358                }
359
360                #[cfg(feature = "e2e-encryption")]
361                if self.is_e2ee_enabled() {
362                    response_processor
363                        .handle_encryption(&sliding_sync_response.extensions, &state_store_guard)
364                        .await?
365                }
366
367                // Only handle the room's subsection of the response, if this sliding sync was
368                // configured to do so.
369                if must_process_rooms_response {
370                    response_processor
371                        .handle_room_response(
372                            &sliding_sync_response,
373                            &requested_required_states,
374                            &state_store_guard,
375                        )
376                        .await?;
377                }
378
379                response_processor
380            };
381
382            // Release the lock before calling event handlers
383            response_processor.process_and_take_response().await?
384        };
385
386        debug!("Sliding Sync response has been handled by the client");
387        trace!(?sync_response);
388
389        let update_summary = {
390            // Update the rooms.
391            let updated_rooms = {
392                let mut updated_rooms = Vec::with_capacity(
393                    sliding_sync_response.rooms.len() + sync_response.rooms.joined.len(),
394                );
395
396                updated_rooms.extend(sliding_sync_response.rooms.keys().cloned());
397
398                // There might be other rooms that were only mentioned in the sliding sync
399                // extensions part of the response, and thus would result in rooms present in
400                // the `sync_response.joined`. Mark them as updated too.
401                //
402                // Since we've removed rooms that were in the room subsection from
403                // `sync_response.rooms.joined`, the remaining ones aren't already present in
404                // `updated_rooms` and wouldn't cause any duplicates.
405                updated_rooms.extend(sync_response.rooms.joined.keys().cloned());
406
407                updated_rooms
408            };
409
410            // Update the lists.
411            let updated_lists = {
412                debug!(
413                    lists = ?sliding_sync_response.lists,
414                    "Update lists"
415                );
416
417                let mut updated_lists = Vec::with_capacity(sliding_sync_response.lists.len());
418                let mut lists = self.inner.lists.write().await;
419
420                // Iterate on known lists, not on lists in the response. Rooms may have been
421                // updated that were not involved in any list update.
422                for (name, list) in lists.iter_mut() {
423                    if let Some(updates) = sliding_sync_response.lists.get(name) {
424                        let maximum_number_of_rooms: u32 =
425                            updates.count.try_into().expect("failed to convert `count` to `u32`");
426
427                        if list.update(Some(maximum_number_of_rooms))? {
428                            updated_lists.push(name.clone());
429                        }
430                    } else if list.update(None)? {
431                        updated_lists.push(name.clone());
432                    }
433                }
434
435                // Report about unknown lists.
436                for name in sliding_sync_response.lists.keys() {
437                    if !lists.contains_key(name) {
438                        error!("Response for list `{name}` - unknown to us; skipping");
439                    }
440                }
441
442                updated_lists
443            };
444
445            UpdateSummary { lists: updated_lists, rooms: updated_rooms }
446        };
447
448        // Everything went well, we can update the position markers.
449        //
450        // Save the new position markers.
451        debug!(previous_pos = position.pos, new_pos = pos, "Updating `pos`");
452
453        position.pos = pos;
454
455        Ok(update_summary)
456    }
457
458    async fn generate_sync_request(
459        &self,
460    ) -> Result<(http::Request, RequestConfig, OwnedMutexGuard<SlidingSyncPositionMarkers>)> {
461        // Collect requests for lists.
462        let mut requests_lists = BTreeMap::new();
463
464        let timeout = {
465            let lists = self.inner.lists.read().await;
466
467            // Start at `Default` in case there is zero list.
468            let mut timeout = PollTimeout::Default;
469
470            for (name, list) in lists.iter() {
471                requests_lists.insert(name.clone(), list.next_request()?);
472                timeout = timeout.min(list.requires_timeout());
473            }
474
475            timeout
476        };
477
478        // Collect the `pos`.
479        //
480        // Wait on the `position` mutex to be available. It means no request nor
481        // response is running. The `position` mutex is released whether the response
482        // has been fully handled successfully, in this case the `pos` is updated, or
483        // the response handling has failed, in this case the `pos` hasn't been updated
484        // and the same `pos` will be used for this new request.
485        let mut position_guard = {
486            debug!("Waiting to acquire the `position` lock");
487
488            let _timer = timer!("acquiring the `position` lock");
489
490            self.inner.position.clone().lock_owned().await
491        };
492
493        debug!(pos = ?position_guard.pos, "Got a position");
494
495        let to_device_enabled = self.inner.extensions.to_device.enabled == Some(true);
496
497        let restored_fields = if self.inner.share_pos || to_device_enabled {
498            restore_sliding_sync_state(&self.inner.client, &self.inner.storage_key).await?
499        } else {
500            None
501        };
502
503        // Update pos: either the one restored from the database, if any and the sliding
504        // sync was configured so, or read it from the memory cache.
505        let pos = if self.inner.share_pos {
506            if let Some(fields) = &restored_fields {
507                // Override the memory one with the database one, for consistency.
508                if fields.pos != position_guard.pos {
509                    info!(
510                        "Pos from previous request ('{:?}') was different from \
511                         pos in database ('{:?}').",
512                        position_guard.pos, fields.pos
513                    );
514                    position_guard.pos = fields.pos.clone();
515                }
516                fields.pos.clone()
517            } else {
518                position_guard.pos.clone()
519            }
520        } else {
521            position_guard.pos.clone()
522        };
523
524        // When the client sends a request with no `pos`, MSC4186 returns no device
525        // lists updates, as it only returns changes since the provided `pos`
526        // (which is `null` in this case); this is in line with sync v2.
527        //
528        // Therefore, with MSC4186, the device list cache must be marked as to be
529        // re-downloaded if the `since` token is `None`, otherwise it's easy to miss
530        // device lists updates that happened between the previous request and the new
531        // “initial” request.
532        #[cfg(feature = "e2e-encryption")]
533        if pos.is_none() && self.is_e2ee_enabled() {
534            info!("Marking all tracked users as dirty");
535
536            let olm_machine = self.inner.client.olm_machine().await;
537            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
538            olm_machine.mark_all_tracked_users_as_dirty().await?;
539        }
540
541        // Configure the timeout.
542        //
543        // The `timeout` query is necessary when all lists require it. Please see
544        // [`SlidingSyncList::requires_timeout`].
545        let timeout = match timeout {
546            PollTimeout::None => None,
547            PollTimeout::Some(timeout) => Some(Duration::from_secs(timeout.into())),
548            PollTimeout::Default => Some(self.inner.poll_timeout),
549        };
550
551        Span::current()
552            .record("pos", &pos)
553            .record("timeout", timeout.map(|duration| duration.as_millis()));
554
555        let mut request = assign!(http::Request::new(), {
556            conn_id: Some(self.inner.id.clone()),
557            pos,
558            set_presence: self.inner.client.sync_presence(),
559            timeout,
560            lists: requests_lists,
561        });
562
563        // Add room subscriptions.
564        request.room_subscriptions = self.inner.room_subscriptions.read().unwrap().clone();
565
566        // Add extensions.
567        request.extensions = self.inner.extensions.clone();
568
569        // Override the to-device token if the extension is enabled.
570        if to_device_enabled {
571            request.extensions.to_device.since =
572                restored_fields.and_then(|fields| fields.to_device_token);
573        }
574
575        Ok((
576            // The request itself.
577            request,
578            // Configure long-polling. We need some time for the long-poll itself,
579            // and extra time for the network delays.
580            RequestConfig::default()
581                .timeout(self.inner.poll_timeout + self.inner.network_timeout)
582                .retry_limit(3),
583            position_guard,
584        ))
585    }
586
587    /// Send a sliding sync request.
588    ///
589    /// This method contains the sending logic.
590    async fn send_sync_request(
591        &self,
592        request: http::Request,
593        request_config: RequestConfig,
594        mut position_guard: OwnedMutexGuard<SlidingSyncPositionMarkers>,
595    ) -> Result<UpdateSummary> {
596        debug!("Sending request");
597
598        // Prepare the request.
599        let requested_required_states = RequestedRequiredStates::from(&request);
600        let request = self.inner.client.send(request).with_request_config(request_config);
601
602        // Send the request and get a response with end-to-end encryption support.
603        //
604        // Sending the `/sync` request out when end-to-end encryption is enabled means
605        // that we need to also send out any outgoing e2ee related request out
606        // coming from the `OlmMachine::outgoing_requests()` method.
607
608        #[cfg(feature = "e2e-encryption")]
609        let response = {
610            if self.is_e2ee_enabled() {
611                // Here, we need to run 2 things:
612                //
613                // 1. Send the sliding sync request and get a response,
614                // 2. Send the E2EE requests.
615                //
616                // We don't want to use a `join` or `try_join` because we want to fail if and
617                // only if sending the sliding sync request fails. Failing to send the E2EE
618                // requests should just result in a log.
619                //
620                // We also want to give the priority to sliding sync request. E2EE requests are
621                // sent concurrently to the sliding sync request, but the priority is on waiting
622                // a sliding sync response.
623                //
624                // If sending sliding sync request fails, the sending of E2EE requests must be
625                // aborted as soon as possible.
626
627                let client = self.inner.client.clone();
628                let e2ee_uploads = spawn(
629                    async move {
630                        if let Err(error) = client.send_outgoing_requests().await {
631                            error!(?error, "Error while sending outgoing E2EE requests");
632                        }
633                    }
634                    .instrument(Span::current()),
635                )
636                // Ensure that the task is not running in detached mode. It is aborted when it's
637                // dropped.
638                .abort_on_drop();
639
640                // Wait on the sliding sync request success or failure early.
641                let response = request.await?;
642
643                // At this point, if `request` has been resolved successfully, we wait on
644                // `e2ee_uploads`. It did run concurrently, so it should not be blocking for too
645                // long. Otherwise —if `request` has failed— `e2ee_uploads` has
646                // been dropped, so aborted.
647                e2ee_uploads.await.map_err(|error| Error::JoinError {
648                    task_description: "e2ee_uploads".to_owned(),
649                    error,
650                })?;
651
652                response
653            } else {
654                request.await?
655            }
656        };
657
658        // Send the request and get a response _without_ end-to-end encryption support.
659        #[cfg(not(feature = "e2e-encryption"))]
660        let response = request.await?;
661
662        debug!("Received response");
663
664        // At this point, the request has been sent, and a response has been received.
665        //
666        // We must ensure the handling of the response cannot be stopped/
667        // cancelled. It must be done entirely, otherwise we can have
668        // corrupted/incomplete states for Sliding Sync and other parts of
669        // the code.
670        //
671        // That's why we are running the handling of the response in a spawned
672        // future that cannot be cancelled by anything.
673        let this = self.clone();
674
675        // Spawn a new future to ensure that the code inside this future cannot be
676        // cancelled if this method is cancelled.
677        let future = async move {
678            debug!("Start handling response");
679
680            // In case the task running this future is detached, we must
681            // ensure responses are handled one at a time. At this point we still own
682            // `position_guard`, so we're fine.
683
684            // Handle the response.
685            let updates = this
686                .handle_response(response, &mut position_guard, requested_required_states)
687                .await?;
688
689            this.cache_to_storage(&position_guard).await?;
690
691            // Release the position guard lock.
692            // It means that other responses can be generated and then handled later.
693            drop(position_guard);
694
695            debug!("Done handling response");
696
697            Ok(updates)
698        };
699
700        spawn(future.instrument(Span::current())).await.map_err(|error| Error::JoinError {
701            task_description: "handle_response".to_owned(),
702            error,
703        })?
704    }
705
706    /// Is the e2ee extension enabled for this sliding sync instance?
707    #[cfg(feature = "e2e-encryption")]
708    fn is_e2ee_enabled(&self) -> bool {
709        self.inner.extensions.e2ee.enabled == Some(true)
710    }
711
712    /// Is the thread subscriptions extension enabled for this sliding sync
713    /// instance?
714    fn is_thread_subscriptions_enabled(&self) -> bool {
715        self.inner.extensions.thread_subscriptions.enabled == Some(true)
716    }
717
718    #[cfg(not(feature = "e2e-encryption"))]
719    fn is_e2ee_enabled(&self) -> bool {
720        false
721    }
722
723    /// Should we process the room's subpart of a response?
724    async fn must_process_rooms_response(&self) -> bool {
725        // We consider that we must, if there's any room subscription or there's any
726        // list.
727        !self.inner.room_subscriptions.read().unwrap().is_empty()
728            || !self.inner.lists.read().await.is_empty()
729    }
730
731    /// Send a single sliding sync request, and returns the response summary.
732    ///
733    /// Public for testing purposes only.
734    #[doc(hidden)]
735    #[instrument(skip_all, fields(conn_id = self.inner.id, pos, timeout))]
736    pub async fn sync_once(&self) -> Result<UpdateSummary> {
737        let (request, request_config, position_guard) = self.generate_sync_request().await?;
738
739        // Send the request.
740        let summaries = self.send_sync_request(request, request_config, position_guard).await?;
741
742        // Notify a new sync was received.
743        self.inner.client.inner.sync_beat.notify(usize::MAX);
744
745        Ok(summaries)
746    }
747
748    /// Create a _new_ Sliding Sync sync loop.
749    ///
750    /// This method returns a `Stream`, which will send requests and will handle
751    /// responses automatically. Lists and rooms are updated automatically.
752    ///
753    /// This function returns `Ok(…)` if everything went well, otherwise it will
754    /// return `Err(…)`. An `Err` will _always_ lead to the `Stream`
755    /// termination.
756    #[allow(unknown_lints, clippy::let_with_type_underscore)] // triggered by instrument macro
757    #[instrument(name = "sync_stream", skip_all, fields(conn_id = self.inner.id, with_e2ee = self.is_e2ee_enabled()))]
758    pub fn sync(&self) -> impl Stream<Item = Result<UpdateSummary, crate::Error>> + '_ {
759        debug!("Starting sync stream");
760
761        let mut internal_channel_receiver = self.inner.internal_channel.subscribe();
762
763        stream! {
764            loop {
765                debug!("Sync stream is running");
766
767                select! {
768                    biased;
769
770                    internal_message = internal_channel_receiver.recv() => {
771                        use SlidingSyncInternalMessage::*;
772
773                        debug!(?internal_message, "Sync stream has received an internal message");
774
775                        match internal_message {
776                            Err(_) | Ok(SyncLoopStop) => {
777                                break;
778                            }
779
780                            Ok(SyncLoopSkipOverCurrentIteration) => {
781                                continue;
782                            }
783                        }
784                    }
785
786                    update_summary = self.sync_once() => {
787                        match update_summary {
788                            Ok(updates) => {
789                                yield Ok(updates);
790                            }
791
792                            // Here, errors we **cannot** ignore, and that must stop the sync loop.
793                            Err(error) => {
794                                if error.client_api_error_kind() == Some(&ErrorKind::UnknownPos) {
795                                    // The Sliding Sync session has expired. Let's reset `pos`.
796                                    self.expire_session().await;
797                                }
798
799                                yield Err(error);
800
801                                // Terminates the loop, and terminates the stream.
802                                break;
803                            }
804                        }
805                    }
806                }
807            }
808
809            debug!("Sync stream has exited.");
810        }
811    }
812
813    /// Force to stop the sync loop ([`Self::sync`]) if it's running.
814    ///
815    /// Usually, dropping the `Stream` returned by [`Self::sync`] should be
816    /// enough to “stop” it, but depending of how this `Stream` is used, it
817    /// might not be obvious to drop it immediately (thinking of using this API
818    /// over FFI; the foreign-language might not be able to drop a value
819    /// immediately). Thus, calling this method will ensure that the sync loop
820    /// stops gracefully and as soon as it returns.
821    pub fn stop_sync(&self) -> Result<()> {
822        Ok(self.inner.internal_channel_send(SlidingSyncInternalMessage::SyncLoopStop)?)
823    }
824
825    /// Expire the current Sliding Sync session on the client-side.
826    ///
827    /// Expiring a Sliding Sync session means: resetting `pos`.
828    ///
829    /// This should only be used when it's clear that this session was about to
830    /// expire anyways, and should be used only in very specific cases (e.g.
831    /// multiple sliding syncs being run in parallel, and one of them has
832    /// expired).
833    ///
834    /// This method **MUST** be called when the sync loop is stopped.
835    #[doc(hidden)]
836    pub async fn expire_session(&self) {
837        info!("Session expired; resetting `pos`");
838
839        {
840            let lists = self.inner.lists.read().await;
841
842            for list in lists.values() {
843                // Invalidate in-memory data that would be persisted on disk.
844                list.set_maximum_number_of_rooms(None);
845            }
846        }
847
848        // Remove the cached sliding sync state as well.
849        {
850            let mut position = self.inner.position.lock().await;
851
852            // Invalidate in memory.
853            position.pos = None;
854
855            // Propagate to disk.
856            // Note: this propagates both the sliding sync state and the cached lists'
857            // state to disk.
858            if let Err(err) = self.cache_to_storage(&position).await {
859                warn!("Failed to invalidate cached sliding sync state: {err}");
860            }
861        }
862
863        {
864            // Clear all room subscriptions: we don't want to resend all room subscriptions
865            // when the session will restart.
866            self.inner.room_subscriptions.write().unwrap().clear();
867        }
868    }
869}
870
871/// Private implementation for [`SlidingSync::subscribe_to_rooms`] and
872/// [`SlidingSync::clear_and_subscribe_to_rooms`].
873fn subscribe_to_rooms(
874    mut room_subscriptions: StdRwLockWriteGuard<
875        '_,
876        BTreeMap<OwnedRoomId, http::request::RoomSubscription>,
877    >,
878    client: &Client,
879    room_ids: &[&RoomId],
880    settings: Option<http::request::RoomSubscription>,
881    cancel_in_flight_request: bool,
882) -> bool {
883    let settings = settings.unwrap_or_default();
884    let mut skip_over_current_sync_loop_iteration = false;
885
886    for room_id in room_ids {
887        if let Entry::Vacant(entry) = room_subscriptions.entry((*room_id).to_owned()) {
888            if let Some(room) = client.get_room(room_id) {
889                room.mark_members_missing();
890            }
891
892            entry.insert(settings.clone());
893
894            skip_over_current_sync_loop_iteration = true;
895        }
896    }
897
898    cancel_in_flight_request && skip_over_current_sync_loop_iteration
899}
900
901impl SlidingSyncInner {
902    /// Send a message over the internal channel.
903    #[instrument]
904    fn internal_channel_send(&self, message: SlidingSyncInternalMessage) -> Result<(), Error> {
905        self.internal_channel.send(message).map(|_| ()).map_err(|_| Error::InternalChannelIsBroken)
906    }
907
908    /// Send a message over the internal channel if there is a receiver, i.e. if
909    /// the sync loop is running.
910    #[instrument]
911    fn internal_channel_send_if_possible(&self, message: SlidingSyncInternalMessage) {
912        // If there is no receiver, the send will fail, but that's OK here.
913        let _ = self.internal_channel.send(message);
914    }
915}
916
917#[derive(Copy, Clone, Debug, PartialEq)]
918enum SlidingSyncInternalMessage {
919    /// Instruct the sync loop to stop.
920    SyncLoopStop,
921
922    /// Instruct the sync loop to skip over any remaining work in its iteration,
923    /// and to jump to the next iteration.
924    SyncLoopSkipOverCurrentIteration,
925}
926
927#[cfg(any(test, feature = "testing"))]
928impl SlidingSync {
929    /// Set a new value for `pos`.
930    pub async fn set_pos(&self, new_pos: String) {
931        let mut position_lock = self.inner.position.lock().await;
932        position_lock.pos = Some(new_pos);
933    }
934}
935
936#[derive(Clone, Debug)]
937pub(super) struct SlidingSyncPositionMarkers {
938    /// An ephemeral position in the current stream, as received from the
939    /// previous `/sync` response, or `None` for the first request.
940    pos: Option<String>,
941}
942
943/// A summary of the updates received after a sync (like in
944/// [`SlidingSync::sync`]).
945#[derive(Debug, Clone)]
946pub struct UpdateSummary {
947    /// The names of the lists that have seen an update.
948    pub lists: Vec<String>,
949    /// The rooms that have seen updates
950    pub rooms: Vec<OwnedRoomId>,
951}
952
953/// Define what kind of poll timeout [`SlidingSync`] must use.
954///
955/// [The spec says about `timeout`][spec]:
956///
957/// > How long to wait for new events […] If omitted the response is always
958/// > returned immediately, even if there are no changes.
959///
960/// [spec]: https://github.com/matrix-org/matrix-spec-proposals/blob/erikj/sss/proposals/4186-simplified-sliding-sync.md#top-level
961#[derive(Debug)]
962pub enum PollTimeout {
963    /// No `timeout` must be present.
964    None,
965
966    /// A `timeout=X` must be present, where `X` is in seconds and
967    /// represents how long to wait for new events.
968    Some(u32),
969
970    /// A `timeout=X` must be present, where `X` is the default value passed to
971    /// [`SlidingSyncBuilder::poll_timeout`].
972    Default,
973}
974
975impl PollTimeout {
976    /// Computes the smallest `PollTimeout` between two of them.
977    ///
978    /// The rules are the following:
979    ///
980    /// * `None` < `Some`,
981    /// * `Some(x) < Some(y)` if and only if `x < y`,
982    /// * `Some < Default`.
983    ///
984    /// The `Default` value is unknown at this step but is assumed to be the
985    /// largest.
986    fn min(self, left: Self) -> Self {
987        match (self, left) {
988            (Self::None, _) => Self::None,
989
990            (Self::Some(_), Self::None) => Self::None,
991            (Self::Some(right), Self::Some(left)) => Self::Some(right.min(left)),
992            (Self::Some(right), Self::Default) => Self::Some(right),
993
994            (Self::Default, Self::None) => Self::None,
995            (Self::Default, Self::Some(left)) => Self::Some(left),
996            (Self::Default, Self::Default) => Self::Default,
997        }
998    }
999}
1000
1001#[cfg(all(test, not(target_family = "wasm")))]
1002#[allow(clippy::dbg_macro)]
1003mod tests {
1004    use std::{
1005        collections::BTreeMap,
1006        future::ready,
1007        ops::Not,
1008        sync::{Arc, Mutex},
1009        time::Duration,
1010    };
1011
1012    use assert_matches::assert_matches;
1013    use event_listener::Listener;
1014    use futures_util::{StreamExt, future::join_all, pin_mut};
1015    use matrix_sdk_base::{RequestedRequiredStates, RoomMemberships};
1016    use matrix_sdk_common::executor::spawn;
1017    use matrix_sdk_test::{ALICE, async_test, event_factory::EventFactory};
1018    use ruma::{
1019        OwnedRoomId, assign,
1020        events::{direct::DirectEvent, room::member::MembershipState},
1021        owned_room_id,
1022        presence::PresenceState,
1023        profile::{
1024            AvatarUrl, DisplayName, ProfileFieldName, UserProfileChanges, UserProfileUpdate,
1025        },
1026        room_id,
1027        serde::Raw,
1028        uint,
1029    };
1030    use serde::Deserialize;
1031    use serde_json::json;
1032    use stream_assert::assert_pending;
1033    use wiremock::{
1034        Match, Mock, MockServer, Request, ResponseTemplate, http::Method, matchers::method,
1035    };
1036
1037    use super::{
1038        SlidingSync, SlidingSyncBuilder, SlidingSyncList, SlidingSyncListBuilder, SlidingSyncMode,
1039        cache::restore_sliding_sync_state, http,
1040    };
1041    use crate::{
1042        Client, Result,
1043        test_utils::{logged_in_client, mocks::MatrixMockServer},
1044    };
1045
1046    #[derive(Copy, Clone)]
1047    struct SlidingSyncMatcher;
1048
1049    impl Match for SlidingSyncMatcher {
1050        fn matches(&self, request: &Request) -> bool {
1051            request.url.path() == "/_matrix/client/unstable/org.matrix.simplified_msc3575/sync"
1052                && request.method == Method::POST
1053        }
1054    }
1055
1056    async fn new_sliding_sync(
1057        lists: Vec<SlidingSyncListBuilder>,
1058    ) -> Result<(MockServer, SlidingSync)> {
1059        let server = MockServer::start().await;
1060        let client = logged_in_client(Some(server.uri())).await;
1061
1062        let mut sliding_sync_builder = client.sliding_sync("test-slidingsync")?;
1063
1064        for list in lists {
1065            sliding_sync_builder = sliding_sync_builder.add_list(list);
1066        }
1067
1068        let sliding_sync = sliding_sync_builder.build().await?;
1069
1070        Ok((server, sliding_sync))
1071    }
1072
1073    #[async_test]
1074    async fn test_subscribe_to_own_profile() {
1075        let client = logged_in_client(None).await;
1076        let own_user_id = client.user_id().expect("client should be logged in").to_owned();
1077
1078        // Given a stored global profile for the current user, received through a
1079        // previous sync.
1080        let mut response = http::Response::new("0".to_owned());
1081
1082        let mut profile_changes = UserProfileChanges::new();
1083        profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Example"));
1084
1085        response
1086            .extensions
1087            .profiles
1088            .users
1089            .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1090        client
1091            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1092            .await
1093            .expect("Failed to process sync");
1094
1095        // Subscribing emits the currently stored value immediately.
1096        let stream = client.subscribe_to_own_profile().expect("client should be logged in");
1097        pin_mut!(stream);
1098
1099        let profile = stream.next().await.expect("should emit the initial profile");
1100        assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1101
1102        // An update for another user only is ignored: nothing is emitted.
1103        let mut response = http::Response::new("1".to_owned());
1104
1105        let mut profile_changes = UserProfileChanges::new();
1106        profile_changes.updated.insert(ProfileFieldName::DisplayName, json!("Alice"));
1107        response
1108            .extensions
1109            .profiles
1110            .users
1111            .insert(ALICE.to_owned(), UserProfileUpdate::Updated(profile_changes));
1112        client
1113            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1114            .await
1115            .expect("Failed to process sync");
1116
1117        assert_pending!(stream);
1118
1119        // An update for the current user is emitted with the merged value.
1120        let mut response = http::Response::new("2".to_owned());
1121
1122        let mut profile_changes = UserProfileChanges::new();
1123        profile_changes
1124            .updated
1125            .insert(ProfileFieldName::AvatarUrl, json!("mxc://example.org/avatar"));
1126
1127        response
1128            .extensions
1129            .profiles
1130            .users
1131            .insert(own_user_id.clone(), UserProfileUpdate::Updated(profile_changes));
1132        client
1133            .process_sliding_sync_test_helper(&response, &RequestedRequiredStates::default())
1134            .await
1135            .expect("Failed to process sync");
1136
1137        let profile = stream.next().await.expect("should emit the updated profile");
1138        assert_eq!(profile.get_static::<DisplayName>().unwrap().as_deref(), Some("Example"));
1139        assert_eq!(
1140            profile.get_static::<AvatarUrl>().unwrap().map(|url| url.to_string()).as_deref(),
1141            Some("mxc://example.org/avatar")
1142        );
1143    }
1144
1145    #[async_test]
1146    async fn test_sliding_sync_request_uses_client_sync_presence() -> Result<()> {
1147        let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
1148        let client = sliding_sync.inner.client.clone();
1149
1150        {
1151            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1152
1153            assert_eq!(request.set_presence, PresenceState::Online);
1154        }
1155
1156        client.set_presence(PresenceState::Unavailable, None, false).await?;
1157
1158        {
1159            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1160
1161            assert_eq!(request.set_presence, PresenceState::Unavailable);
1162        }
1163
1164        client.set_presence(PresenceState::Offline, None, false).await?;
1165
1166        {
1167            let (request, _, _position_guard) = sliding_sync.generate_sync_request().await?;
1168
1169            assert_eq!(request.set_presence, PresenceState::Offline);
1170        }
1171
1172        Ok(())
1173    }
1174
1175    #[async_test]
1176    async fn test_subscribe_to_rooms() -> Result<()> {
1177        let (server, sliding_sync) = new_sliding_sync(vec![
1178            SlidingSyncList::builder("foo")
1179                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1180        ])
1181        .await?;
1182
1183        let stream = sliding_sync.sync();
1184        pin_mut!(stream);
1185
1186        let room_id_0 = room_id!("!r0:bar.org");
1187        let room_id_1 = room_id!("!r1:bar.org");
1188        let room_id_2 = room_id!("!r2:bar.org");
1189
1190        {
1191            let _mock_guard = Mock::given(SlidingSyncMatcher)
1192                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1193                    "pos": "1",
1194                    "lists": {},
1195                    "rooms": {
1196                        room_id_0: {
1197                            "name": "Room #0",
1198                            "initial": true,
1199                        },
1200                        room_id_1: {
1201                            "name": "Room #1",
1202                            "initial": true,
1203                        },
1204                        room_id_2: {
1205                            "name": "Room #2",
1206                            "initial": true,
1207                        },
1208                    }
1209                })))
1210                .mount_as_scoped(&server)
1211                .await;
1212
1213            let _ = stream.next().await.unwrap()?;
1214        }
1215
1216        let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1217
1218        // Members aren't synced.
1219        // We need to make them synced, so that we can test that subscribing to a room
1220        // make members not synced. That's a desired feature.
1221        assert!(room0.are_members_synced().not());
1222
1223        {
1224            struct MemberMatcher(OwnedRoomId);
1225
1226            impl Match for MemberMatcher {
1227                fn matches(&self, request: &Request) -> bool {
1228                    request.url.path()
1229                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1230                        && request.method == Method::GET
1231                }
1232            }
1233
1234            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1235                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1236                    "chunk": [],
1237                })))
1238                .mount_as_scoped(&server)
1239                .await;
1240
1241            assert_matches!(room0.request_members().await, Ok(()));
1242        }
1243
1244        // Members are now synced! We can start subscribing and see how it goes.
1245        assert!(room0.are_members_synced());
1246
1247        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1248
1249        // OK, we have subscribed to some rooms. Let's check on `room0` if members are
1250        // now marked as not synced.
1251        assert!(room0.are_members_synced().not());
1252
1253        {
1254            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1255
1256            assert!(room_subscriptions.contains_key(room_id_0));
1257            assert!(room_subscriptions.contains_key(room_id_1));
1258            assert!(!room_subscriptions.contains_key(room_id_2));
1259        }
1260
1261        // Subscribing to the same room doesn't reset the member sync state.
1262
1263        {
1264            struct MemberMatcher(OwnedRoomId);
1265
1266            impl Match for MemberMatcher {
1267                fn matches(&self, request: &Request) -> bool {
1268                    request.url.path()
1269                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1270                        && request.method == Method::GET
1271                }
1272            }
1273
1274            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1275                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1276                    "chunk": [],
1277                })))
1278                .mount_as_scoped(&server)
1279                .await;
1280
1281            assert_matches!(room0.request_members().await, Ok(()));
1282        }
1283
1284        // Members are synced, good, good.
1285        assert!(room0.are_members_synced());
1286
1287        sliding_sync.subscribe_to_rooms(&[room_id_0], None, false);
1288
1289        // Members are still synced: because we have already subscribed to the
1290        // room, the members aren't marked as unsynced.
1291        assert!(room0.are_members_synced());
1292
1293        Ok(())
1294    }
1295
1296    #[async_test]
1297    async fn test_subscribe_unsubscribe_and_clear_and_subscribe_to_rooms() -> Result<()> {
1298        let (_server, sliding_sync) = new_sliding_sync(vec![
1299            SlidingSyncList::builder("foo")
1300                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1301        ])
1302        .await?;
1303
1304        let room_id_0 = room_id!("!r0:bar.org");
1305        let room_id_1 = room_id!("!r1:bar.org");
1306        let room_id_2 = room_id!("!r2:bar.org");
1307        let room_id_3 = room_id!("!r3:bar.org");
1308
1309        // Initially empty.
1310        {
1311            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1312
1313            assert!(room_subscriptions.is_empty());
1314        }
1315
1316        // Add 2 rooms.
1317        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1318
1319        {
1320            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1321
1322            assert_eq!(room_subscriptions.len(), 2);
1323            assert!(room_subscriptions.contains_key(room_id_0));
1324            assert!(room_subscriptions.contains_key(room_id_1));
1325        }
1326
1327        // Remove 1 room.
1328        sliding_sync.unsubscribe_to_rooms(&[room_id_0], false);
1329
1330        {
1331            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1332
1333            assert_eq!(room_subscriptions.len(), 1);
1334            assert!(room_subscriptions.contains_key(room_id_1));
1335        }
1336
1337        // Add 2 rooms, but one already exists.
1338        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], Default::default(), false);
1339
1340        {
1341            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1342
1343            assert_eq!(room_subscriptions.len(), 2);
1344            assert!(room_subscriptions.contains_key(room_id_0));
1345            assert!(room_subscriptions.contains_key(room_id_1));
1346        }
1347
1348        // Replace all rooms with 2 other rooms.
1349        sliding_sync.clear_and_subscribe_to_rooms(
1350            &[room_id_2, room_id_3],
1351            Default::default(),
1352            false,
1353        );
1354
1355        {
1356            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1357
1358            assert_eq!(room_subscriptions.len(), 2);
1359            assert!(room_subscriptions.contains_key(room_id_2));
1360            assert!(room_subscriptions.contains_key(room_id_3));
1361        }
1362
1363        Ok(())
1364    }
1365
1366    #[async_test]
1367    async fn test_resubscribe_to_rooms() -> Result<()> {
1368        let (server, sliding_sync) = new_sliding_sync(vec![
1369            SlidingSyncList::builder("foo")
1370                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1371        ])
1372        .await?;
1373
1374        let stream = sliding_sync.sync();
1375        pin_mut!(stream);
1376
1377        let room_id_0 = room_id!("!r0:bar.org");
1378        let room_id_1 = room_id!("!r1:bar.org");
1379        let room_id_2 = room_id!("!r2:bar.org");
1380
1381        {
1382            let _mock_guard = Mock::given(SlidingSyncMatcher)
1383                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1384                    "pos": "1",
1385                    "lists": {},
1386                    "rooms": {
1387                        room_id_0: {
1388                            "name": "Room #0",
1389                            "initial": true,
1390                        },
1391                        room_id_1: {
1392                            "name": "Room #1",
1393                            "initial": true,
1394                        },
1395                        room_id_2: {
1396                            "name": "Room #2",
1397                            "initial": true,
1398                        },
1399                    }
1400                })))
1401                .mount_as_scoped(&server)
1402                .await;
1403
1404            let _ = stream.next().await.unwrap()?;
1405        }
1406
1407        let room0 = sliding_sync.inner.client.get_room(room_id_0).unwrap();
1408
1409        // Members aren't synced.
1410        // We need to make them synced, so that we can test that subscribing to a room
1411        // make members not synced. That's a desired feature.
1412        assert!(room0.are_members_synced().not());
1413
1414        {
1415            struct MemberMatcher(OwnedRoomId);
1416
1417            impl Match for MemberMatcher {
1418                fn matches(&self, request: &Request) -> bool {
1419                    request.url.path()
1420                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1421                        && request.method == Method::GET
1422                }
1423            }
1424
1425            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1426                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1427                    "chunk": [],
1428                })))
1429                .mount_as_scoped(&server)
1430                .await;
1431
1432            assert_matches!(room0.request_members().await, Ok(()));
1433        }
1434
1435        // Members are now synced! We can start subscribing and see how it goes.
1436        assert!(room0.are_members_synced());
1437
1438        sliding_sync.resubscribe_to_rooms(&[room_id_0, room_id_1], None, true);
1439
1440        // OK, we have subscribed to some rooms. Let's check on `room0` if members are
1441        // now marked as not synced.
1442        assert!(room0.are_members_synced().not());
1443
1444        // Both resubscribed rooms are subscribed.
1445        {
1446            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1447
1448            assert!(room_subscriptions.contains_key(room_id_0));
1449            assert!(room_subscriptions.contains_key(room_id_1));
1450            assert!(!room_subscriptions.contains_key(room_id_2));
1451        }
1452
1453        // Subscribing to the same room doesn't reset the member sync state.
1454
1455        {
1456            struct MemberMatcher(OwnedRoomId);
1457
1458            impl Match for MemberMatcher {
1459                fn matches(&self, request: &Request) -> bool {
1460                    request.url.path()
1461                        == format!("/_matrix/client/r0/rooms/{room_id}/members", room_id = self.0)
1462                        && request.method == Method::GET
1463                }
1464            }
1465
1466            let _mock_guard = Mock::given(MemberMatcher(room_id_0.to_owned()))
1467                .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1468                    "chunk": [],
1469                })))
1470                .mount_as_scoped(&server)
1471                .await;
1472
1473            assert_matches!(room0.request_members().await, Ok(()));
1474        }
1475
1476        // Members are synced, good, good.
1477        assert!(room0.are_members_synced());
1478
1479        sliding_sync.resubscribe_to_rooms(&[room_id_0], None, false);
1480
1481        // Members are still synced: because we have already subscribed to the
1482        // room, the members aren't marked as unsynced.
1483        assert!(room0.are_members_synced());
1484
1485        // Only the resubscribed room is subscribed.
1486        {
1487            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1488
1489            assert!(room_subscriptions.contains_key(room_id_0));
1490            assert!(!room_subscriptions.contains_key(room_id_1));
1491            assert!(!room_subscriptions.contains_key(room_id_2));
1492        }
1493        Ok(())
1494    }
1495
1496    #[async_test]
1497    async fn test_room_subscriptions_are_reset_when_session_expires() -> Result<()> {
1498        let (_server, sliding_sync) = new_sliding_sync(vec![
1499            SlidingSyncList::builder("foo")
1500                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1501        ])
1502        .await?;
1503
1504        let room_id_0 = room_id!("!r0:bar.org");
1505        let room_id_1 = room_id!("!r1:bar.org");
1506        let room_id_2 = room_id!("!r2:bar.org");
1507
1508        // Subscribe to two rooms.
1509        sliding_sync.subscribe_to_rooms(&[room_id_0, room_id_1], None, false);
1510
1511        {
1512            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1513
1514            assert!(room_subscriptions.contains_key(room_id_0));
1515            assert!(room_subscriptions.contains_key(room_id_1));
1516            assert!(room_subscriptions.contains_key(room_id_2).not());
1517        }
1518
1519        // Subscribe to one more room.
1520        sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1521
1522        {
1523            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1524
1525            assert!(room_subscriptions.contains_key(room_id_0));
1526            assert!(room_subscriptions.contains_key(room_id_1));
1527            assert!(room_subscriptions.contains_key(room_id_2));
1528        }
1529
1530        // Suddenly, the session expires!
1531        sliding_sync.expire_session().await;
1532
1533        {
1534            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1535
1536            assert!(room_subscriptions.is_empty());
1537        }
1538
1539        // Subscribe to one room again.
1540        sliding_sync.subscribe_to_rooms(&[room_id_2], None, false);
1541
1542        {
1543            let room_subscriptions = sliding_sync.inner.room_subscriptions.read().unwrap();
1544
1545            assert!(room_subscriptions.contains_key(room_id_0).not());
1546            assert!(room_subscriptions.contains_key(room_id_1).not());
1547            assert!(room_subscriptions.contains_key(room_id_2));
1548        }
1549
1550        Ok(())
1551    }
1552
1553    #[async_test]
1554    async fn test_add_list() -> Result<()> {
1555        let (_server, sliding_sync) = new_sliding_sync(vec![
1556            SlidingSyncList::builder("foo")
1557                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1558        ])
1559        .await?;
1560
1561        let _stream = sliding_sync.sync();
1562        pin_mut!(_stream);
1563
1564        sliding_sync
1565            .add_list(
1566                SlidingSyncList::builder("bar")
1567                    .sync_mode(SlidingSyncMode::new_selective().add_range(50..=60)),
1568            )
1569            .await?;
1570
1571        let lists = sliding_sync.inner.lists.read().await;
1572
1573        assert!(lists.contains_key("foo"));
1574        assert!(lists.contains_key("bar"));
1575
1576        // this test also ensures that Tokio is not panicking when calling `add_list`.
1577
1578        Ok(())
1579    }
1580
1581    #[cfg(feature = "e2e-encryption")]
1582    #[async_test]
1583    async fn test_extensions_to_device_since_is_set() {
1584        use matrix_sdk_base::crypto::store::types::Changes;
1585
1586        let client = logged_in_client(None).await;
1587        let sliding_sync = SlidingSyncBuilder::new("foo".to_owned(), client.clone())
1588            .unwrap()
1589            .with_to_device_extension(assign!(
1590                http::request::ToDevice::default(),
1591                {
1592                    enabled: Some(true),
1593                }
1594            ))
1595            .build()
1596            .await
1597            .unwrap();
1598
1599        // Test `SlidingSyncInner::extensions`.
1600        {
1601            let to_device = &sliding_sync.inner.extensions.to_device;
1602
1603            assert_eq!(to_device.enabled, Some(true));
1604            assert!(to_device.since.is_none());
1605        }
1606
1607        // Test `Request::extensions`.
1608        {
1609            let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1610
1611            let to_device = &request.extensions.to_device;
1612
1613            assert_eq!(to_device.enabled, Some(true));
1614            assert!(to_device.since.is_none());
1615        }
1616
1617        // Define a `since` token.
1618        let since_token = "depuis".to_owned();
1619
1620        {
1621            if let Some(olm_machine) = &*client.olm_machine().await {
1622                olm_machine
1623                    .store()
1624                    .save_changes(Changes {
1625                        next_batch_token: Some(since_token.clone()),
1626                        ..Default::default()
1627                    })
1628                    .await
1629                    .unwrap();
1630            } else {
1631                panic!("Where is the Olm machine?");
1632            }
1633        }
1634
1635        // Test `Request::extensions` again.
1636        {
1637            let (request, _, _) = sliding_sync.generate_sync_request().await.unwrap();
1638
1639            let to_device = &request.extensions.to_device;
1640
1641            assert_eq!(to_device.enabled, Some(true));
1642            assert_eq!(to_device.since, Some(since_token));
1643        }
1644    }
1645
1646    // With MSC4186, with the `e2ee` extension enabled, if a request has no `pos`,
1647    // all the tracked users by the `OlmMachine` must be marked as dirty, i.e.
1648    // `/key/query` requests must be sent. See the code to see the details.
1649    //
1650    // This test is asserting that.
1651    #[async_test]
1652    #[cfg(feature = "e2e-encryption")]
1653    async fn test_no_pos_with_e2ee_marks_all_tracked_users_as_dirty() -> anyhow::Result<()> {
1654        use matrix_sdk_base::crypto::types::requests::{AnyIncomingResponse, AnyOutgoingRequest};
1655        use matrix_sdk_test::ruma_response_from_json;
1656        use ruma::user_id;
1657
1658        let server = MockServer::start().await;
1659        let client = logged_in_client(Some(server.uri())).await;
1660
1661        let alice = user_id!("@alice:localhost");
1662        let bob = user_id!("@bob:localhost");
1663        let me = user_id!("@example:localhost");
1664
1665        // Track and mark users are not dirty, so that we can check they are “dirty”
1666        // after that. Dirty here means that a `/key/query` must be sent.
1667        {
1668            let olm_machine = client.olm_machine().await;
1669            let olm_machine = olm_machine.as_ref().unwrap();
1670
1671            olm_machine.update_tracked_users([alice, bob]).await?;
1672
1673            // Assert requests.
1674            let outgoing_requests = olm_machine.outgoing_requests().await?;
1675
1676            assert_eq!(outgoing_requests.len(), 2);
1677            assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysUpload(_));
1678            assert_matches!(outgoing_requests[1].request(), AnyOutgoingRequest::KeysQuery(_));
1679
1680            // Fake responses.
1681            olm_machine
1682                .mark_request_as_sent(
1683                    outgoing_requests[0].request_id(),
1684                    AnyIncomingResponse::KeysUpload(&ruma_response_from_json(&json!({
1685                        "one_time_key_counts": {}
1686                    }))),
1687                )
1688                .await?;
1689
1690            olm_machine
1691                .mark_request_as_sent(
1692                    outgoing_requests[1].request_id(),
1693                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1694                        "device_keys": {
1695                            alice: {},
1696                            bob: {},
1697                        }
1698                    }))),
1699                )
1700                .await?;
1701
1702            // Once more.
1703            let outgoing_requests = olm_machine.outgoing_requests().await?;
1704
1705            assert_eq!(outgoing_requests.len(), 1);
1706            assert_matches!(outgoing_requests[0].request(), AnyOutgoingRequest::KeysQuery(_));
1707
1708            olm_machine
1709                .mark_request_as_sent(
1710                    outgoing_requests[0].request_id(),
1711                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1712                        "device_keys": {
1713                            me: {},
1714                        }
1715                    }))),
1716                )
1717                .await?;
1718
1719            // No more.
1720            let outgoing_requests = olm_machine.outgoing_requests().await?;
1721
1722            assert!(outgoing_requests.is_empty());
1723        }
1724
1725        let sync = client
1726            .sliding_sync("test-slidingsync")?
1727            .add_list(SlidingSyncList::builder("new_list"))
1728            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
1729            .build()
1730            .await?;
1731
1732        // First request: no `pos`.
1733        let (_request, _, _) = sync.generate_sync_request().await?;
1734
1735        // Now, tracked users must be dirty.
1736        {
1737            let olm_machine = client.olm_machine().await;
1738            let olm_machine = olm_machine.as_ref().unwrap();
1739
1740            // Assert requests.
1741            let outgoing_requests = olm_machine.outgoing_requests().await?;
1742
1743            assert_eq!(outgoing_requests.len(), 1);
1744            assert_matches!(
1745                outgoing_requests[0].request(),
1746                AnyOutgoingRequest::KeysQuery(request) => {
1747                    assert!(request.device_keys.contains_key(alice));
1748                    assert!(request.device_keys.contains_key(bob));
1749                    assert!(request.device_keys.contains_key(me));
1750                }
1751            );
1752
1753            // Fake responses.
1754            olm_machine
1755                .mark_request_as_sent(
1756                    outgoing_requests[0].request_id(),
1757                    AnyIncomingResponse::KeysQuery(&ruma_response_from_json(&json!({
1758                        "device_keys": {
1759                            alice: {},
1760                            bob: {},
1761                            me: {},
1762                        }
1763                    }))),
1764                )
1765                .await?;
1766        }
1767
1768        // Second request: with a `pos` this time.
1769        sync.set_pos("chocolat".to_owned()).await;
1770
1771        let (_request, _, _) = sync.generate_sync_request().await?;
1772
1773        // Tracked users are not marked as dirty.
1774        {
1775            let olm_machine = client.olm_machine().await;
1776            let olm_machine = olm_machine.as_ref().unwrap();
1777
1778            // Assert requests.
1779            let outgoing_requests = olm_machine.outgoing_requests().await?;
1780
1781            assert!(outgoing_requests.is_empty());
1782        }
1783
1784        Ok(())
1785    }
1786
1787    #[cfg(feature = "e2e-encryption")]
1788    #[async_test]
1789    async fn test_sliding_sync_doesnt_remember_pos() -> Result<()> {
1790        let server = MockServer::start().await;
1791
1792        #[derive(Deserialize)]
1793        struct PartialRequest {
1794            txn_id: Option<String>,
1795        }
1796
1797        let server_pos = Arc::new(Mutex::new(0));
1798        let _mock_guard = Mock::given(SlidingSyncMatcher)
1799            .respond_with(move |request: &Request| {
1800                // Repeat the txn_id in the response, if set.
1801                let request: PartialRequest = request.body_json().unwrap();
1802                let pos = {
1803                    let mut pos = server_pos.lock().unwrap();
1804                    let prev = *pos;
1805                    *pos += 1;
1806                    prev
1807                };
1808
1809                ResponseTemplate::new(200).set_body_json(json!({
1810                    "txn_id": request.txn_id,
1811                    "pos": pos.to_string(),
1812                }))
1813            })
1814            .mount_as_scoped(&server)
1815            .await;
1816
1817        let client = logged_in_client(Some(server.uri())).await;
1818
1819        let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1820
1821        // `pos` is `None` to start with.
1822        {
1823            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1824
1825            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1826            assert!(request.pos.is_none());
1827        }
1828
1829        let sync = sliding_sync.sync();
1830        pin_mut!(sync);
1831
1832        // Sync goes well, and then the position is saved both into the internal memory
1833        // and the database.
1834        let next = sync.next().await;
1835        assert_matches!(next, Some(Ok(_update_summary)));
1836
1837        assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1838
1839        let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1840            .await?
1841            .expect("must have restored fields");
1842
1843        // While it has been saved into the database, it's not necessarily going to be
1844        // used later!
1845        assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1846
1847        // Now, even if we mess with the position stored in the database, the sliding
1848        // sync instance isn't configured to reload the stream position from the
1849        // database, so it won't be changed.
1850        {
1851            let other_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1852
1853            let mut position_guard = other_sync.inner.position.lock().await;
1854            position_guard.pos = Some("yolo".to_owned());
1855
1856            other_sync.cache_to_storage(&position_guard).await?;
1857        }
1858
1859        // It's still 0, not "yolo".
1860        {
1861            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("0"));
1862            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1863            assert_eq!(request.pos.as_deref(), Some("0"));
1864        }
1865
1866        // Recreating a sliding sync with the same ID doesn't preload the pos, if not
1867        // asked to.
1868        {
1869            let sliding_sync = client.sliding_sync("forgetful-sync")?.build().await?;
1870            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1871        }
1872
1873        Ok(())
1874    }
1875
1876    #[cfg(feature = "e2e-encryption")]
1877    #[async_test]
1878    async fn test_sliding_sync_does_remember_pos() -> Result<()> {
1879        let server = MockServer::start().await;
1880
1881        #[derive(Deserialize)]
1882        struct PartialRequest {
1883            txn_id: Option<String>,
1884        }
1885
1886        let server_pos = Arc::new(Mutex::new(0));
1887        let _mock_guard = Mock::given(SlidingSyncMatcher)
1888            .respond_with(move |request: &Request| {
1889                // Repeat the txn_id in the response, if set.
1890                let request: PartialRequest = request.body_json().unwrap();
1891                let pos = {
1892                    let mut pos = server_pos.lock().unwrap();
1893                    let prev = *pos;
1894                    *pos += 1;
1895                    prev
1896                };
1897
1898                ResponseTemplate::new(200).set_body_json(json!({
1899                    "txn_id": request.txn_id,
1900                    "pos": pos.to_string(),
1901                }))
1902            })
1903            .mount_as_scoped(&server)
1904            .await;
1905
1906        let client = logged_in_client(Some(server.uri())).await;
1907
1908        let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1909
1910        // `pos` is `None` to start with.
1911        {
1912            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1913
1914            assert!(request.pos.is_none());
1915            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1916        }
1917
1918        let sync = sliding_sync.sync();
1919        pin_mut!(sync);
1920
1921        // Sync goes well, and then the position is saved both into the internal memory
1922        // and the database.
1923        let next = sync.next().await;
1924        assert_matches!(next, Some(Ok(_update_summary)));
1925
1926        assert_eq!(sliding_sync.inner.position.lock().await.pos, Some("0".to_owned()));
1927
1928        let restored_fields = restore_sliding_sync_state(&client, &sliding_sync.inner.storage_key)
1929            .await?
1930            .expect("must have restored fields");
1931
1932        // While it has been saved into the database, it's not necessarily going to be
1933        // used later!
1934        assert_eq!(restored_fields.pos.as_deref(), Some("0"));
1935
1936        // Another process modifies the stream position under our feet...
1937        {
1938            let other_sync = client.sliding_sync("elephant-sync")?.build().await?;
1939
1940            let mut position_guard = other_sync.inner.position.lock().await;
1941            position_guard.pos = Some("42".to_owned());
1942
1943            other_sync.cache_to_storage(&position_guard).await?;
1944        }
1945
1946        // It's alright, the next request will load it from the database.
1947        {
1948            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1949            assert_eq!(request.pos.as_deref(), Some("42"));
1950            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1951        }
1952
1953        // Recreating a sliding sync with the same ID will reload it too.
1954        {
1955            let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1956            assert_eq!(sliding_sync.inner.position.lock().await.pos.as_deref(), Some("42"));
1957
1958            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1959            assert_eq!(request.pos.as_deref(), Some("42"));
1960        }
1961
1962        // Invalidating the session will remove the in-memory value AND the database
1963        // value.
1964        sliding_sync.expire_session().await;
1965
1966        {
1967            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1968
1969            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1970            assert!(request.pos.is_none());
1971        }
1972
1973        // And new sliding syncs with the same ID won't find it either.
1974        {
1975            let sliding_sync = client.sliding_sync("elephant-sync")?.share_pos().build().await?;
1976            assert!(sliding_sync.inner.position.lock().await.pos.is_none());
1977
1978            let (request, _, _) = sliding_sync.generate_sync_request().await?;
1979            assert!(request.pos.is_none());
1980        }
1981
1982        Ok(())
1983    }
1984
1985    #[async_test]
1986    async fn test_stop_sync_loop() -> Result<()> {
1987        let (_server, sliding_sync) = new_sliding_sync(vec![
1988            SlidingSyncList::builder("foo")
1989                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
1990        ])
1991        .await?;
1992
1993        // Start the sync loop.
1994        let stream = sliding_sync.sync();
1995        pin_mut!(stream);
1996
1997        // The sync loop is actually running.
1998        assert!(stream.next().await.is_some());
1999
2000        // Stop the sync loop.
2001        sliding_sync.stop_sync()?;
2002
2003        // The sync loop is actually stopped.
2004        assert!(stream.next().await.is_none());
2005
2006        // Start a new sync loop.
2007        let stream = sliding_sync.sync();
2008        pin_mut!(stream);
2009
2010        // The sync loop is actually running.
2011        assert!(stream.next().await.is_some());
2012
2013        Ok(())
2014    }
2015
2016    #[async_test]
2017    async fn test_process_read_receipts() -> Result<()> {
2018        let room = owned_room_id!("!pony:example.org");
2019
2020        let server = MockServer::start().await;
2021        let client = logged_in_client(Some(server.uri())).await;
2022        client.event_cache().subscribe().unwrap();
2023
2024        let sliding_sync = client
2025            .sliding_sync("test")?
2026            .with_receipt_extension(
2027                assign!(http::request::Receipts::default(), { enabled: Some(true) }),
2028            )
2029            .add_list(
2030                SlidingSyncList::builder("all")
2031                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2032            )
2033            .build()
2034            .await?;
2035
2036        // Initial state.
2037        {
2038            let server_response = assign!(http::Response::new("0".to_owned()), {
2039                rooms: BTreeMap::from([(
2040                    room.clone(),
2041                    http::response::Room::default(),
2042                )])
2043            });
2044
2045            let _summary = {
2046                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2047                sliding_sync
2048                    .handle_response(
2049                        server_response.clone(),
2050                        &mut pos_guard,
2051                        RequestedRequiredStates::default(),
2052                    )
2053                    .await?
2054            };
2055        }
2056
2057        let server_response = assign!(http::Response::new("1".to_owned()), {
2058            extensions: assign!(http::response::Extensions::default(), {
2059                receipts: assign!(http::response::Receipts::default(), {
2060                    rooms: BTreeMap::from([
2061                        (
2062                            room.clone(),
2063                            Raw::from_json_string(
2064                                json!({
2065                                    "room_id": room,
2066                                    "type": "m.receipt",
2067                                    "content": {
2068                                        "$event:bar.org": {
2069                                            "m.read": {
2070                                                client.user_id().unwrap(): {
2071                                                    "ts": 1436451550,
2072                                                }
2073                                            }
2074                                        }
2075                                    }
2076                                })
2077                                .to_string(),
2078                            ).unwrap()
2079                        )
2080                    ])
2081                })
2082            })
2083        });
2084
2085        let summary = {
2086            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2087            sliding_sync
2088                .handle_response(
2089                    server_response.clone(),
2090                    &mut pos_guard,
2091                    RequestedRequiredStates::default(),
2092                )
2093                .await?
2094        };
2095
2096        assert!(summary.rooms.contains(&room));
2097
2098        Ok(())
2099    }
2100
2101    #[async_test]
2102    async fn test_process_marked_unread_room_account_data() -> Result<()> {
2103        let room_id = owned_room_id!("!unicorn:example.org");
2104
2105        let server = MockServer::start().await;
2106        let client = logged_in_client(Some(server.uri())).await;
2107
2108        // Setup sliding sync with with one room and one list
2109
2110        let sliding_sync = client
2111            .sliding_sync("test")?
2112            .with_account_data_extension(
2113                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2114            )
2115            .add_list(
2116                SlidingSyncList::builder("all")
2117                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2118            )
2119            .build()
2120            .await?;
2121
2122        // Initial state.
2123        {
2124            let server_response = assign!(http::Response::new("0".to_owned()), {
2125                rooms: BTreeMap::from([(
2126                    room_id.clone(),
2127                    http::response::Room::default(),
2128                )])
2129            });
2130
2131            let _summary = {
2132                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2133                sliding_sync
2134                    .handle_response(
2135                        server_response.clone(),
2136                        &mut pos_guard,
2137                        RequestedRequiredStates::default(),
2138                    )
2139                    .await?
2140            };
2141        }
2142
2143        // Simulate a response that only changes the marked unread state of the room to
2144        // true
2145
2146        let server_response = make_mark_unread_response("1", room_id.clone(), true, false);
2147
2148        let update_summary = {
2149            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2150            sliding_sync
2151                .handle_response(
2152                    server_response.clone(),
2153                    &mut pos_guard,
2154                    RequestedRequiredStates::default(),
2155                )
2156                .await?
2157        };
2158
2159        // Check that the list list and entry received the update
2160
2161        assert!(update_summary.rooms.contains(&room_id));
2162
2163        let room = client.get_room(&room_id).unwrap();
2164
2165        // Check the actual room data, this powers RoomInfo
2166
2167        assert!(room.is_marked_unread());
2168
2169        // Change it back to false and check if it updates
2170
2171        let server_response = make_mark_unread_response("2", room_id.clone(), false, true);
2172
2173        let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2174        sliding_sync
2175            .handle_response(
2176                server_response.clone(),
2177                &mut pos_guard,
2178                RequestedRequiredStates::default(),
2179            )
2180            .await?;
2181
2182        let room = client.get_room(&room_id).unwrap();
2183
2184        assert!(!room.is_marked_unread());
2185
2186        Ok(())
2187    }
2188
2189    fn make_mark_unread_response(
2190        response_number: &str,
2191        room_id: OwnedRoomId,
2192        unread: bool,
2193        add_rooms_section: bool,
2194    ) -> http::Response {
2195        let rooms = if add_rooms_section {
2196            BTreeMap::from([(room_id.clone(), http::response::Room::default())])
2197        } else {
2198            BTreeMap::new()
2199        };
2200
2201        let extensions = assign!(http::response::Extensions::default(), {
2202            account_data: assign!(http::response::AccountData::default(), {
2203                rooms: BTreeMap::from([
2204                    (
2205                        room_id,
2206                        vec![
2207                            Raw::from_json_string(
2208                                json!({
2209                                    "content": {
2210                                        "unread": unread
2211                                    },
2212                                    "type": "m.marked_unread"
2213                                })
2214                                .to_string(),
2215                            ).unwrap()
2216                        ]
2217                    )
2218                ])
2219            })
2220        });
2221
2222        assign!(http::Response::new(response_number.to_owned()), { rooms: rooms, extensions: extensions })
2223    }
2224
2225    #[async_test]
2226    async fn test_process_rooms_account_data() -> Result<()> {
2227        let room = owned_room_id!("!pony:example.org");
2228
2229        let server = MockServer::start().await;
2230        let client = logged_in_client(Some(server.uri())).await;
2231
2232        let sliding_sync = client
2233            .sliding_sync("test")?
2234            .with_account_data_extension(
2235                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2236            )
2237            .add_list(
2238                SlidingSyncList::builder("all")
2239                    .sync_mode(SlidingSyncMode::new_selective().add_range(0..=100)),
2240            )
2241            .build()
2242            .await?;
2243
2244        // Initial state.
2245        {
2246            let server_response = assign!(http::Response::new("0".to_owned()), {
2247                rooms: BTreeMap::from([(
2248                    room.clone(),
2249                    http::response::Room::default(),
2250                )])
2251            });
2252
2253            let _summary = {
2254                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2255                sliding_sync
2256                    .handle_response(
2257                        server_response.clone(),
2258                        &mut pos_guard,
2259                        RequestedRequiredStates::default(),
2260                    )
2261                    .await?
2262            };
2263        }
2264
2265        let server_response = assign!(http::Response::new("1".to_owned()), {
2266            extensions: assign!(http::response::Extensions::default(), {
2267                account_data: assign!(http::response::AccountData::default(), {
2268                    rooms: BTreeMap::from([
2269                        (
2270                            room.clone(),
2271                            vec![
2272                                Raw::from_json_string(
2273                                    json!({
2274                                        "content": {
2275                                            "tags": {
2276                                                "u.work": {
2277                                                    "order": 0.9
2278                                                }
2279                                            }
2280                                        },
2281                                        "type": "m.tag"
2282                                    })
2283                                    .to_string(),
2284                                ).unwrap()
2285                            ]
2286                        )
2287                    ])
2288                })
2289            })
2290        });
2291        let summary = {
2292            let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2293            sliding_sync
2294                .handle_response(
2295                    server_response.clone(),
2296                    &mut pos_guard,
2297                    RequestedRequiredStates::default(),
2298                )
2299                .await?
2300        };
2301
2302        assert!(summary.rooms.contains(&room));
2303
2304        Ok(())
2305    }
2306
2307    #[async_test]
2308    #[cfg(feature = "e2e-encryption")]
2309    async fn test_process_only_encryption_events() -> Result<()> {
2310        use ruma::OneTimeKeyAlgorithm;
2311
2312        let room = owned_room_id!("!croissant:example.org");
2313
2314        let server = MockServer::start().await;
2315        let client = logged_in_client(Some(server.uri())).await;
2316
2317        let server_response = assign!(http::Response::new("0".to_owned()), {
2318            rooms: BTreeMap::from([(
2319                room.clone(),
2320                assign!(http::response::Room::default(), {
2321                    name: Some("Croissants lovers".to_owned()),
2322                    timeline: Vec::new(),
2323                }),
2324            )]),
2325
2326            extensions: assign!(http::response::Extensions::default(), {
2327                e2ee: assign!(http::response::E2EE::default(), {
2328                    device_one_time_keys_count: BTreeMap::from([(OneTimeKeyAlgorithm::SignedCurve25519, uint!(42))])
2329                }),
2330                to_device: Some(assign!(http::response::ToDevice::default(), {
2331                    next_batch: "to-device-token".to_owned(),
2332                })),
2333            })
2334        });
2335
2336        // Don't process non-encryption events if the sliding sync is configured for
2337        // encryption only.
2338
2339        let sliding_sync = client
2340            .sliding_sync("test")?
2341            .with_to_device_extension(
2342                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2343            )
2344            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2345            .build()
2346            .await?;
2347
2348        {
2349            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2350
2351            sliding_sync
2352                .handle_response(
2353                    server_response.clone(),
2354                    &mut position_guard,
2355                    RequestedRequiredStates::default(),
2356                )
2357                .await?;
2358        }
2359
2360        // E2EE has been properly handled.
2361        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2362        assert_eq!(uploaded_key_count, 42);
2363
2364        {
2365            let olm_machine = &*client.olm_machine_for_testing().await;
2366            assert_eq!(
2367                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2368                Some("to-device-token")
2369            );
2370        }
2371
2372        // Room events haven't.
2373        assert!(client.get_room(&room).is_none());
2374
2375        // Conversely, only process room lists events if the sliding sync was configured
2376        // as so.
2377        let client = logged_in_client(Some(server.uri())).await;
2378
2379        let sliding_sync = client
2380            .sliding_sync("test")?
2381            .add_list(SlidingSyncList::builder("thelist"))
2382            .build()
2383            .await?;
2384
2385        {
2386            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2387
2388            sliding_sync
2389                .handle_response(
2390                    server_response.clone(),
2391                    &mut position_guard,
2392                    RequestedRequiredStates::default(),
2393                )
2394                .await?;
2395        }
2396
2397        // E2EE response has been ignored.
2398        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2399        assert_eq!(uploaded_key_count, 0);
2400
2401        {
2402            let olm_machine = &*client.olm_machine_for_testing().await;
2403            assert_eq!(
2404                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2405                None
2406            );
2407        }
2408
2409        // The room is now known.
2410        assert!(client.get_room(&room).is_some());
2411
2412        // And it's also possible to set up both.
2413        let client = logged_in_client(Some(server.uri())).await;
2414
2415        let sliding_sync = client
2416            .sliding_sync("test")?
2417            .add_list(SlidingSyncList::builder("thelist"))
2418            .with_to_device_extension(
2419                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2420            )
2421            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2422            .build()
2423            .await?;
2424
2425        {
2426            let mut position_guard = sliding_sync.inner.position.clone().lock_owned().await;
2427
2428            sliding_sync
2429                .handle_response(
2430                    server_response.clone(),
2431                    &mut position_guard,
2432                    RequestedRequiredStates::default(),
2433                )
2434                .await?;
2435        }
2436
2437        // E2EE has been properly handled.
2438        let uploaded_key_count = client.encryption().uploaded_key_count().await?;
2439        assert_eq!(uploaded_key_count, 42);
2440
2441        {
2442            let olm_machine = &*client.olm_machine_for_testing().await;
2443            assert_eq!(
2444                olm_machine.as_ref().unwrap().store().next_batch_token().await?.as_deref(),
2445                Some("to-device-token")
2446            );
2447        }
2448
2449        // The room is now known.
2450        assert!(client.get_room(&room).is_some());
2451
2452        Ok(())
2453    }
2454
2455    #[async_test]
2456    async fn test_lock_multiple_requests() -> Result<()> {
2457        let server = MockServer::start().await;
2458        let client = logged_in_client(Some(server.uri())).await;
2459
2460        let pos = Arc::new(Mutex::new(0));
2461        let _mock_guard = Mock::given(SlidingSyncMatcher)
2462            .respond_with(move |_: &Request| {
2463                let mut pos = pos.lock().unwrap();
2464                *pos += 1;
2465                ResponseTemplate::new(200).set_body_json(json!({
2466                    "pos": pos.to_string(),
2467                    "lists": {},
2468                    "rooms": {}
2469                }))
2470            })
2471            .mount_as_scoped(&server)
2472            .await;
2473
2474        let sliding_sync = client
2475            .sliding_sync("test")?
2476            .with_to_device_extension(
2477                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2478            )
2479            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2480            .build()
2481            .await?;
2482
2483        // Spawn two requests in parallel. Before #2430, this lead to a deadlock and the
2484        // test would never terminate.
2485        let requests = join_all([sliding_sync.sync_once(), sliding_sync.sync_once()]);
2486
2487        for result in requests.await {
2488            result?;
2489        }
2490
2491        Ok(())
2492    }
2493
2494    #[async_test]
2495    async fn test_aborted_request_doesnt_update_future_requests() -> Result<()> {
2496        let server = MockServer::start().await;
2497        let client = logged_in_client(Some(server.uri())).await;
2498
2499        let pos = Arc::new(Mutex::new(0));
2500        let _mock_guard = Mock::given(SlidingSyncMatcher)
2501            .respond_with(move |_: &Request| {
2502                let mut pos = pos.lock().unwrap();
2503                *pos += 1;
2504                // Respond slowly enough that we can skip one iteration.
2505                ResponseTemplate::new(200)
2506                    .set_body_json(json!({
2507                        "pos": pos.to_string(),
2508                        "lists": {},
2509                        "rooms": {}
2510                    }))
2511                    .set_delay(Duration::from_secs(2))
2512            })
2513            .mount_as_scoped(&server)
2514            .await;
2515
2516        let sliding_sync =
2517            client
2518                .sliding_sync("test")?
2519                .add_list(SlidingSyncList::builder("room-list").sync_mode(
2520                    SlidingSyncMode::new_growing(10).maximum_number_of_rooms_to_fetch(100),
2521                ))
2522                .add_list(
2523                    SlidingSyncList::builder("another-list")
2524                        .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2525                )
2526                .build()
2527                .await?;
2528
2529        let stream = sliding_sync.sync();
2530        pin_mut!(stream);
2531
2532        let cloned_sync = sliding_sync.clone();
2533        spawn(async move {
2534            tokio::time::sleep(Duration::from_millis(100)).await;
2535
2536            cloned_sync
2537                .on_list("another-list", |list| {
2538                    list.set_sync_mode(SlidingSyncMode::new_selective().add_range(10..=20));
2539                    ready(())
2540                })
2541                .await;
2542        });
2543
2544        assert_matches!(stream.next().await, Some(Ok(_)));
2545
2546        sliding_sync.stop_sync().unwrap();
2547
2548        assert_matches!(stream.next().await, None);
2549
2550        let mut num_requests = 0;
2551
2552        for request in server.received_requests().await.unwrap() {
2553            if !SlidingSyncMatcher.matches(&request) {
2554                continue;
2555            }
2556
2557            let another_list_ranges = if num_requests == 0 {
2558                // First request
2559                json!([[0, 10]])
2560            } else {
2561                // Second request
2562                json!([[10, 20]])
2563            };
2564
2565            num_requests += 1;
2566            assert!(num_requests <= 2, "more than one request hit the server");
2567
2568            let json_value = serde_json::from_slice::<serde_json::Value>(&request.body).unwrap();
2569
2570            if let Err(err) = assert_json_diff::assert_json_matches_no_panic(
2571                &json_value,
2572                &json!({
2573                    "conn_id": "test",
2574                    "lists": {
2575                        "room-list": {
2576                            "ranges": [[0, 9]],
2577                            "required_state": [
2578                                ["m.room.encryption", ""],
2579                                ["m.room.tombstone", ""]
2580                            ],
2581                        },
2582                        "another-list": {
2583                            "ranges": another_list_ranges,
2584                            "required_state": [
2585                                ["m.room.encryption", ""],
2586                                ["m.room.tombstone", ""]
2587                            ],
2588                        },
2589                    }
2590                }),
2591                assert_json_diff::Config::new(assert_json_diff::CompareMode::Inclusive),
2592            ) {
2593                dbg!(json_value);
2594                panic!("json differ: {err}");
2595            }
2596        }
2597
2598        assert_eq!(num_requests, 2);
2599
2600        Ok(())
2601    }
2602
2603    #[async_test]
2604    async fn test_timeout_zero_list() -> Result<()> {
2605        let (_server, sliding_sync) = new_sliding_sync(vec![]).await?;
2606
2607        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2608
2609        // Zero list means sliding sync is fully loaded, so there is a timeout to wait
2610        // on new update to pop.
2611        assert!(request.timeout.is_some());
2612
2613        Ok(())
2614    }
2615
2616    #[async_test]
2617    async fn test_timeout_one_list() -> Result<()> {
2618        let (_server, sliding_sync) = new_sliding_sync(vec![
2619            SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2620        ])
2621        .await?;
2622
2623        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2624
2625        // The list does not require a timeout.
2626        assert!(request.timeout.is_none());
2627
2628        // Simulate a response.
2629        {
2630            let server_response = assign!(http::Response::new("0".to_owned()), {
2631                lists: BTreeMap::from([(
2632                    "foo".to_owned(),
2633                    assign!(http::response::List::default(), {
2634                        count: uint!(7),
2635                    })
2636                 )])
2637            });
2638
2639            let _summary = {
2640                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2641                sliding_sync
2642                    .handle_response(
2643                        server_response.clone(),
2644                        &mut pos_guard,
2645                        RequestedRequiredStates::default(),
2646                    )
2647                    .await?
2648            };
2649        }
2650
2651        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2652
2653        // The list is now fully loaded, so it requires a timeout.
2654        assert!(request.timeout.is_some());
2655
2656        Ok(())
2657    }
2658
2659    #[async_test]
2660    async fn test_timeout_three_lists() -> Result<()> {
2661        let (_server, sliding_sync) = new_sliding_sync(vec![
2662            SlidingSyncList::builder("foo").sync_mode(SlidingSyncMode::new_growing(10)),
2663            SlidingSyncList::builder("bar").sync_mode(SlidingSyncMode::new_paging(10)),
2664            SlidingSyncList::builder("baz")
2665                .sync_mode(SlidingSyncMode::new_selective().add_range(0..=10)),
2666        ])
2667        .await?;
2668
2669        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2670
2671        // Two lists don't require a timeout.
2672        assert!(request.timeout.is_none());
2673
2674        // Simulate a response.
2675        {
2676            let server_response = assign!(http::Response::new("0".to_owned()), {
2677                lists: BTreeMap::from([(
2678                    "foo".to_owned(),
2679                    assign!(http::response::List::default(), {
2680                        count: uint!(7),
2681                    })
2682                 )])
2683            });
2684
2685            let _summary = {
2686                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2687                sliding_sync
2688                    .handle_response(
2689                        server_response.clone(),
2690                        &mut pos_guard,
2691                        RequestedRequiredStates::default(),
2692                    )
2693                    .await?
2694            };
2695        }
2696
2697        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2698
2699        // One don't require a timeout.
2700        assert!(request.timeout.is_none());
2701
2702        // Simulate a response.
2703        {
2704            let server_response = assign!(http::Response::new("1".to_owned()), {
2705                lists: BTreeMap::from([(
2706                    "bar".to_owned(),
2707                    assign!(http::response::List::default(), {
2708                        count: uint!(7),
2709                    })
2710                 )])
2711            });
2712
2713            let _summary = {
2714                let mut pos_guard = sliding_sync.inner.position.clone().lock_owned().await;
2715                sliding_sync
2716                    .handle_response(
2717                        server_response.clone(),
2718                        &mut pos_guard,
2719                        RequestedRequiredStates::default(),
2720                    )
2721                    .await?
2722            };
2723        }
2724
2725        let (request, _, _) = sliding_sync.generate_sync_request().await?;
2726
2727        // All lists require a timeout.
2728        assert!(request.timeout.is_some());
2729
2730        Ok(())
2731    }
2732
2733    #[async_test]
2734    async fn test_sync_beat_is_notified_on_sync_response() -> Result<()> {
2735        let server = MockServer::start().await;
2736        let client = logged_in_client(Some(server.uri())).await;
2737
2738        let _mock_guard = Mock::given(SlidingSyncMatcher)
2739            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2740                "pos": "0",
2741                "lists": {},
2742                "rooms": {}
2743            })))
2744            .mount_as_scoped(&server)
2745            .await;
2746
2747        let sliding_sync = client
2748            .sliding_sync("test")?
2749            .with_to_device_extension(
2750                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2751            )
2752            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2753            .build()
2754            .await?;
2755
2756        let sliding_sync = Arc::new(sliding_sync);
2757
2758        // Create the listener and perform a sync request
2759        let sync_beat_listener = client.inner.sync_beat.listen();
2760        sliding_sync.sync_once().await?;
2761
2762        // The sync beat listener should be notified shortly after
2763        assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_some());
2764        Ok(())
2765    }
2766
2767    #[async_test]
2768    async fn test_sync_beat_is_not_notified_on_sync_failure() -> Result<()> {
2769        let server = MockServer::start().await;
2770        let client = logged_in_client(Some(server.uri())).await;
2771
2772        let _mock_guard = Mock::given(SlidingSyncMatcher)
2773            .respond_with(ResponseTemplate::new(404))
2774            .mount_as_scoped(&server)
2775            .await;
2776
2777        let sliding_sync = client
2778            .sliding_sync("test")?
2779            .with_to_device_extension(
2780                assign!(http::request::ToDevice::default(), { enabled: Some(true)}),
2781            )
2782            .with_e2ee_extension(assign!(http::request::E2EE::default(), { enabled: Some(true)}))
2783            .build()
2784            .await?;
2785
2786        let sliding_sync = Arc::new(sliding_sync);
2787
2788        // Create the listener and perform a sync request
2789        let sync_beat_listener = client.inner.sync_beat.listen();
2790        let sync_result = sliding_sync.sync_once().await;
2791        assert!(sync_result.is_err());
2792
2793        // The sync beat listener won't be notified in this case
2794        assert!(sync_beat_listener.wait_timeout(Duration::from_secs(1)).is_none());
2795
2796        Ok(())
2797    }
2798
2799    #[async_test]
2800    async fn test_state_store_lock_is_released_before_calling_handlers() -> Result<()> {
2801        let server = MatrixMockServer::new().await;
2802        let client = server.client_builder().build().await;
2803        let room_id = room_id!("!mu5hr00m:example.org");
2804
2805        let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2806            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2807                "pos": "0",
2808                "lists": {},
2809                "extensions": {
2810                    "account_data": {
2811                        "global": [
2812                            {
2813                                "type": "m.direct",
2814                                "content": {
2815                                    "@de4dlockh0lmes:example.org": [
2816                                        "!mu5hr00m:example.org"
2817                                    ]
2818                                }
2819                            }
2820                        ]
2821                    }
2822                },
2823                "rooms": {
2824                    room_id: {
2825                        "name": "Mario Bros Fanbase Room",
2826                        "initial": true,
2827                    },
2828                }
2829            })))
2830            .mount_as_scoped(server.server())
2831            .await;
2832
2833        let f = EventFactory::new().room(room_id);
2834
2835        Mock::given(method("GET"))
2836            .and(wiremock::matchers::path_regex(r"/_matrix/client/v3/rooms/.*/members"))
2837            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
2838                "chunk": [
2839                    f.member(&ALICE).membership(MembershipState::Join).into_raw_timeline(),
2840                ]
2841            })))
2842            .mount(server.server())
2843            .await;
2844
2845        let (tx, rx) = tokio::sync::oneshot::channel();
2846
2847        let tx = Arc::new(Mutex::new(Some(tx)));
2848        client.add_event_handler(move |_: DirectEvent, client: Client| async move {
2849            // Try to run a /members query while in a event handler.
2850            let members =
2851                client.get_room(room_id).unwrap().members(RoomMemberships::JOIN).await.unwrap();
2852            assert_eq!(members.len(), 1);
2853            tx.lock().unwrap().take().expect("sender consumed multiple times").send(()).unwrap();
2854        });
2855
2856        let sliding_sync = client
2857            .sliding_sync("test")?
2858            .add_list(SlidingSyncList::builder("thelist"))
2859            .with_account_data_extension(
2860                assign!(http::request::AccountData::default(), { enabled: Some(true) }),
2861            )
2862            .build()
2863            .await?;
2864
2865        tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
2866            .await
2867            .expect("Sync did not complete in time")
2868            .expect("Sync failed");
2869
2870        // Wait for the event handler to complete.
2871        tokio::time::timeout(Duration::from_secs(5), rx)
2872            .await
2873            .expect("Event handler did not complete in time")
2874            .expect("Event handler failed");
2875
2876        Ok(())
2877    }
2878
2879    #[cfg(feature = "e2e-encryption")]
2880    #[async_test]
2881    async fn test_syncing_one_time_key_counts_updates() -> Result<()> {
2882        macro_rules! assert_key_count {
2883            ($client: ident, $count:literal) => {{
2884                let machine = $client.olm_machine().await;
2885                let uploaded_key_counts =
2886                    machine.as_ref().unwrap().uploaded_key_count().await.unwrap();
2887                assert_eq!(uploaded_key_counts, $count)
2888            }};
2889        }
2890
2891        macro_rules! sync_with_key_count {
2892            ($client: ident, $server:ident, $count:literal) => {
2893                let count = Some($count);
2894                sync_with_key_count!($client, $server, count);
2895            };
2896            ($client: ident, $server:ident, $count:ident) => {{
2897                let count: Option<u32> = $count;
2898
2899                let template = if let Some(count) = count {
2900                    ResponseTemplate::new(200).set_body_json(json!({
2901                                        "pos": "0",
2902                                        "lists": {},
2903                                        "extensions": {
2904                                            "e2ee": {
2905                                                "device_one_time_keys_count": {
2906                                                    "signed_curve25519": count,
2907                                                }
2908                                            }
2909                                        },
2910                    }))
2911                } else {
2912                    ResponseTemplate::new(200).set_body_json(json!({
2913                                        "pos": "0",
2914                                        "lists": {},
2915                                        "extensions": {
2916                                            "e2ee": {}
2917                                        },
2918                    }))
2919                };
2920
2921                let _sync_mock_guard = Mock::given(SlidingSyncMatcher)
2922                    .respond_with(template)
2923                    .mount_as_scoped($server.server())
2924                    .await;
2925
2926                let sliding_sync = $client
2927                    .sliding_sync("test")?
2928                    .with_e2ee_extension(
2929                        assign!(http::request::E2EE::default(), { enabled: Some(true)}),
2930                    )
2931                    .build()
2932                    .await?;
2933
2934                tokio::time::timeout(Duration::from_secs(5), sliding_sync.sync_once())
2935                    .await
2936                    .expect("Sync did not complete in time")
2937                    .expect("Sync failed");
2938            }}
2939        }
2940
2941        let server = MatrixMockServer::new().await;
2942        let client = server.client_builder().build().await;
2943
2944        server.mock_upload_keys().ok_with_signed_curve_key_count(50).mock_once().mount().await;
2945
2946        // In the beginning there were no uploaded keys.
2947        assert_key_count!(client, 0);
2948
2949        // The first sync will upload 50 one-time keys.
2950        sync_with_key_count!(client, server, None);
2951        assert_key_count!(client, 50);
2952
2953        // Syncing with no key count will not modify the local key count.
2954        sync_with_key_count!(client, server, None);
2955        assert_key_count!(client, 50);
2956
2957        // Syncing with a key count, will update the key count.
2958        sync_with_key_count!(client, server, 10);
2959        assert_key_count!(client, 10);
2960
2961        Ok(())
2962    }
2963}