Skip to main content

matrix_sdk_base/
client.rs

1// Copyright 2020 Damir Jelić
2// Copyright 2020 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 the specific language governing permissions and
14// limitations under the License.
15
16#[cfg(feature = "e2e-encryption")]
17use std::sync::Arc;
18use std::{
19    collections::{BTreeMap, BTreeSet, HashMap},
20    fmt,
21    ops::Deref,
22};
23
24use eyeball::{SharedObservable, Subscriber};
25use eyeball_im::{Vector, VectorDiff};
26use futures_util::Stream;
27use matrix_sdk_common::{cross_process_lock::CrossProcessLockConfig, timer};
28#[cfg(feature = "experimental-x509-identity-verification")]
29use matrix_sdk_crypto::x509::{RawX509Signer, RawX509Verifier};
30#[cfg(feature = "e2e-encryption")]
31use matrix_sdk_crypto::{
32    CollectStrategy, DecryptionSettings, EncryptionSettings, OlmError, OlmMachine,
33    OlmMachineBuilder, TrustRequirement, store::DynCryptoStore,
34    store::types::RoomPendingKeyBundleDetails, types::requests::ToDeviceRequest,
35};
36#[cfg(doc)]
37use ruma::DeviceId;
38#[cfg(feature = "e2e-encryption")]
39use ruma::events::room::{history_visibility::HistoryVisibility, member::MembershipState};
40use ruma::{
41    OwnedRoomId, OwnedUserId, RoomId, UserId,
42    api::client::{self as api, sync::sync_events::v5},
43    events::{
44        StateEvent, StateEventType,
45        ignored_user_list::IgnoredUserListEventContent,
46        push_rules::{PushRulesEvent, PushRulesEventContent},
47        room::member::SyncRoomMemberEvent,
48    },
49    push::Ruleset,
50    time::Instant,
51};
52use tokio::sync::{Mutex, broadcast};
53#[cfg(feature = "e2e-encryption")]
54use tokio::sync::{RwLock, RwLockReadGuard};
55use tracing::{Level, debug, enabled, info, instrument, warn};
56
57#[cfg(feature = "e2e-encryption")]
58use crate::RoomMemberships;
59use crate::{
60    RoomStateFilter, SessionMeta, StateStore,
61    deserialized_responses::DisplayName,
62    error::{Error, Result},
63    event_cache::store::EventCacheStoreLock,
64    media::store::MediaStoreLock,
65    response_processors::{self as processors, Context},
66    room::{
67        Room, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomMembersUpdate, RoomState,
68    },
69    store::{
70        AvatarCache, BaseStateStore, DynStateStore, MemoryStore, Result as StoreResult,
71        RoomLoadSettings, StateChanges, StateStoreDataKey, StateStoreDataValue, StateStoreExt,
72        StoreConfig, ambiguity_map::AmbiguityCache,
73    },
74    sync::{RoomUpdates, SyncResponse},
75};
76
77/// A no (network) IO client implementation.
78///
79/// This client is a state machine that receives responses and events and
80/// accordingly updates its state. It is not designed to be used directly, but
81/// rather through `matrix_sdk::Client`.
82///
83/// ```rust
84/// use matrix_sdk_base::{
85///     BaseClient, DmRoomDefinition, ThreadingSupport, store::StoreConfig,
86/// };
87/// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
88///
89/// let client = BaseClient::new(
90///     StoreConfig::new(CrossProcessLockConfig::multi_process(
91///         "cross-process-holder-name".to_owned(),
92///     )),
93///     ThreadingSupport::Disabled,
94///     DmRoomDefinition::default(),
95/// );
96/// ```
97#[derive(Clone)]
98pub struct BaseClient {
99    /// The state store.
100    pub(crate) state_store: BaseStateStore,
101
102    /// The store used by the event cache.
103    event_cache_store: EventCacheStoreLock,
104
105    /// The store used by the media cache.
106    media_store: MediaStoreLock,
107
108    /// The store used for encryption.
109    ///
110    /// This field is only meant to be used for `OlmMachine` initialization.
111    /// All operations on it happen inside the `OlmMachine`.
112    #[cfg(feature = "e2e-encryption")]
113    crypto_store: Arc<DynCryptoStore>,
114
115    /// The olm-machine that is created once the
116    /// [`SessionMeta`][crate::session::SessionMeta] is set via
117    /// [`BaseClient::activate`]
118    #[cfg(feature = "e2e-encryption")]
119    olm_machine: Arc<RwLock<Option<OlmMachine>>>,
120
121    /// Observable of when a user is ignored/unignored.
122    pub(crate) ignore_user_list_changes: SharedObservable<Vec<String>>,
123
124    /// Broadcasts the user IDs whose global profile changed during a sync.
125    /// Requires the Profiles sliding sync extension to be enabled.
126    pub(crate) global_profile_updates_sender: broadcast::Sender<BTreeSet<OwnedUserId>>,
127
128    /// The strategy to use for picking recipient devices, when sending an
129    /// encrypted message.
130    #[cfg(feature = "e2e-encryption")]
131    pub room_key_recipient_strategy: CollectStrategy,
132
133    /// The settings to use for decrypting events.
134    #[cfg(feature = "e2e-encryption")]
135    pub decryption_settings: DecryptionSettings,
136
137    /// If the client should handle verification events received when syncing.
138    #[cfg(feature = "e2e-encryption")]
139    pub handle_verification_events: bool,
140
141    /// Whether the client supports threads or not.
142    pub threading_support: ThreadingSupport,
143
144    /// If supported, the signer that allows us to sign our cross-signing key
145    /// with an X.509 certificate.
146    #[cfg(feature = "experimental-x509-identity-verification")]
147    x509_signer: Option<Arc<dyn RawX509Signer>>,
148
149    /// If supported, the verifier that allows us to verify that items have been
150    /// signed by a valid X.509 certificate.
151    #[cfg(feature = "experimental-x509-identity-verification")]
152    x509_verifier: Option<Arc<dyn RawX509Verifier>>,
153
154    /// The definition of what is considered a DM room.
155    pub dm_room_definition: DmRoomDefinition,
156}
157
158#[cfg(not(tarpaulin_include))]
159impl fmt::Debug for BaseClient {
160    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161        f.debug_struct("BaseClient")
162            .field("session_meta", &self.state_store.session_meta())
163            .field("sync_token", &self.state_store.sync_token)
164            .finish_non_exhaustive()
165    }
166}
167
168/// Whether this client instance supports threading or not. Currently used to
169/// determine how the client handles read receipts and unread count computations
170/// on the base SDK level.
171///
172/// Timelines on the other hand have a separate `TimelineFocus`
173/// `hide_threaded_events` associated value that can be used to hide threaded
174/// events but also to enable threaded read receipt sending. This is because
175/// certain timeline instances should ignore threading no matter what's defined
176/// at the client level. One such example are media filtered timelines which
177/// should contain all the room's media no matter what thread its in (unless
178/// explicitly opted into).
179#[derive(Clone, Copy, Debug)]
180pub enum ThreadingSupport {
181    /// Threading enabled.
182    Enabled {
183        /// Enable client-wide thread subscriptions support (MSC4306 / MSC4308).
184        ///
185        /// This may cause filtering out of thread subscriptions, and loading
186        /// the thread subscriptions via the sliding sync extension,
187        /// when the room list service is being used.
188        with_subscriptions: bool,
189    },
190    /// Threading disabled.
191    Disabled,
192}
193
194impl BaseClient {
195    /// Create a new client.
196    ///
197    /// # Arguments
198    ///
199    /// * `config` - the configuration for the stores (state store, event cache
200    ///   store and crypto store).
201    pub fn new(
202        config: StoreConfig,
203        threading_support: ThreadingSupport,
204        dm_room_definition: DmRoomDefinition,
205    ) -> Self {
206        let store = BaseStateStore::new(config.state_store);
207
208        BaseClient {
209            state_store: store,
210            event_cache_store: config.event_cache_store,
211            media_store: config.media_store,
212            #[cfg(feature = "e2e-encryption")]
213            crypto_store: config.crypto_store,
214            #[cfg(feature = "e2e-encryption")]
215            olm_machine: Default::default(),
216            ignore_user_list_changes: Default::default(),
217            global_profile_updates_sender: broadcast::Sender::new(16),
218            #[cfg(feature = "e2e-encryption")]
219            room_key_recipient_strategy: Default::default(),
220            #[cfg(feature = "e2e-encryption")]
221            decryption_settings: DecryptionSettings {
222                sender_device_trust_requirement: TrustRequirement::Untrusted,
223            },
224            #[cfg(feature = "e2e-encryption")]
225            handle_verification_events: true,
226            threading_support,
227            #[cfg(feature = "experimental-x509-identity-verification")]
228            x509_signer: None,
229            #[cfg(feature = "experimental-x509-identity-verification")]
230            x509_verifier: None,
231            dm_room_definition,
232        }
233    }
234
235    /// Clones the current base client to use the same crypto store but a
236    /// different, in-memory store config, and resets transient state.
237    #[cfg(feature = "e2e-encryption")]
238    pub async fn clone_with_in_memory_state_store(
239        &self,
240        cross_process_mode: CrossProcessLockConfig,
241        handle_verification_events: bool,
242    ) -> Result<Self> {
243        let config = StoreConfig::new(cross_process_mode).state_store(MemoryStore::new());
244        let config = config.crypto_store(self.crypto_store.clone());
245
246        let copy = Self {
247            state_store: BaseStateStore::new(config.state_store),
248            event_cache_store: config.event_cache_store,
249            media_store: config.media_store,
250            // We copy the crypto store as well as the `OlmMachine` for two reasons:
251            // 1. The `self.crypto_store` is the same as the one used inside the `OlmMachine`.
252            // 2. We need to ensure that the parent and child use the same data and caches inside
253            //    the `OlmMachine` so the various ratchets and places where new randomness gets
254            //    introduced don't diverge, i.e. one-time keys that get generated by the Olm Account
255            //    or Olm sessions when they encrypt or decrypt messages.
256            crypto_store: self.crypto_store.clone(),
257            olm_machine: self.olm_machine.clone(),
258            ignore_user_list_changes: Default::default(),
259            global_profile_updates_sender: broadcast::Sender::new(16),
260            room_key_recipient_strategy: self.room_key_recipient_strategy.clone(),
261            decryption_settings: self.decryption_settings.clone(),
262            handle_verification_events,
263            threading_support: self.threading_support,
264            #[cfg(feature = "experimental-x509-identity-verification")]
265            x509_signer: self.x509_signer.clone(),
266            #[cfg(feature = "experimental-x509-identity-verification")]
267            x509_verifier: self.x509_verifier.clone(),
268            dm_room_definition: self.dm_room_definition.clone(),
269        };
270
271        copy.state_store.derive_from_other(&self.state_store).await?;
272
273        Ok(copy)
274    }
275
276    /// Provide the signer we will use to sign master signing keys and outgoing
277    /// secret requests.
278    #[cfg(feature = "experimental-x509-identity-verification")]
279    pub fn set_x509_signer(&mut self, x509_signer: Option<Arc<dyn RawX509Signer>>) {
280        self.x509_signer = x509_signer;
281    }
282
283    /// Provide the verifier we will use to verify master signing keys and
284    /// incoming secret requests.
285    #[cfg(feature = "experimental-x509-identity-verification")]
286    pub fn set_x509_verifier(&mut self, x509_verifier: Option<Arc<dyn RawX509Verifier>>) {
287        self.x509_verifier = x509_verifier
288    }
289
290    /// Clones the current base client to use the same crypto store but a
291    /// different, in-memory store config, and resets transient state.
292    #[cfg(not(feature = "e2e-encryption"))]
293    #[allow(clippy::unused_async)]
294    pub async fn clone_with_in_memory_state_store(
295        &self,
296        cross_process_store_config: CrossProcessLockConfig,
297        _handle_verification_events: bool,
298    ) -> Result<Self> {
299        let config = StoreConfig::new(cross_process_store_config).state_store(MemoryStore::new());
300        Ok(Self::new(config, ThreadingSupport::Disabled, DmRoomDefinition::default()))
301    }
302
303    /// Get the session meta information.
304    ///
305    /// If the client is currently logged in, this will return a
306    /// [`SessionMeta`] object which contains the user ID and device ID.
307    /// Otherwise it returns `None`.
308    pub fn session_meta(&self) -> Option<&SessionMeta> {
309        self.state_store.session_meta()
310    }
311
312    /// Get all the rooms this client knows about.
313    pub fn rooms(&self) -> Vec<Room> {
314        self.state_store.rooms()
315    }
316
317    /// Get all the rooms this client knows about, filtered by room state.
318    pub fn rooms_filtered(&self, filter: RoomStateFilter) -> Vec<Room> {
319        self.state_store.rooms_filtered(filter)
320    }
321
322    /// Get a stream of all the rooms changes, in addition to the existing
323    /// rooms.
324    pub fn rooms_stream(
325        &self,
326    ) -> (Vector<Room>, impl Stream<Item = Vec<VectorDiff<Room>>> + use<>) {
327        self.state_store.rooms_stream()
328    }
329
330    /// Lookup the Room for the given RoomId, or create one, if it didn't exist
331    /// yet in the store
332    pub fn get_or_create_room(&self, room_id: &RoomId, room_state: RoomState) -> Room {
333        self.state_store.get_or_create_room(room_id, room_state)
334    }
335
336    /// Get a reference to the state store.
337    pub fn state_store(&self) -> &DynStateStore {
338        self.state_store.deref()
339    }
340
341    /// Get a reference to the event cache store.
342    pub fn event_cache_store(&self) -> &EventCacheStoreLock {
343        &self.event_cache_store
344    }
345
346    /// Get a reference to the media store.
347    pub fn media_store(&self) -> &MediaStoreLock {
348        &self.media_store
349    }
350
351    /// Check whether the client has been activated.
352    ///
353    /// See [`BaseClient::activate`] to know what it means.
354    pub fn is_active(&self) -> bool {
355        self.state_store.session_meta().is_some()
356    }
357
358    /// Activate the client.
359    ///
360    /// A client is considered active when:
361    ///
362    /// 1. It has a `SessionMeta` (user ID, device ID and access token),
363    /// 2. Has loaded cached data from storage,
364    /// 3. If encryption is enabled, it also initialized or restored its
365    ///    `OlmMachine`.
366    ///
367    /// # Arguments
368    ///
369    /// * `session_meta` - The meta of a session that the user already has from
370    ///   a previous login call.
371    ///
372    /// * `custom_account` - A custom
373    ///   [`matrix_sdk_crypto::vodozemac::olm::Account`] to be used for the
374    ///   identity and one-time keys of this [`BaseClient`]. If no account is
375    ///   provided, a new default one or one from the store will be used. If an
376    ///   account is provided and one already exists in the store for this
377    ///   [`UserId`]/[`DeviceId`] combination, an error will be raised. This is
378    ///   useful if one wishes to create identity keys before knowing the
379    ///   user/device IDs, e.g., to use the identity key as the device ID.
380    ///
381    /// * `room_load_settings` — Specify how many rooms must be restored; use
382    ///   `::default()` if you don't know which value to pick.
383    ///
384    /// # Panics
385    ///
386    /// This method panics if it is called twice.
387    ///
388    /// [`UserId`]: ruma::UserId
389    pub async fn activate(
390        &self,
391        session_meta: SessionMeta,
392        room_load_settings: RoomLoadSettings,
393        #[cfg(feature = "e2e-encryption")] custom_account: Option<
394            crate::crypto::vodozemac::olm::Account,
395        >,
396    ) -> Result<()> {
397        debug!(user_id = ?session_meta.user_id, device_id = ?session_meta.device_id, "Activating the client");
398
399        self.state_store.load_rooms(&session_meta.user_id, room_load_settings).await?;
400        self.state_store.load_sync_token().await?;
401        self.state_store.set_session_meta(session_meta);
402
403        #[cfg(feature = "e2e-encryption")]
404        self.regenerate_olm(custom_account).await?;
405
406        Ok(())
407    }
408
409    /// Recreate an `OlmMachine` from scratch.
410    ///
411    /// In particular, this will clear all its caches.
412    #[cfg(feature = "e2e-encryption")]
413    pub async fn regenerate_olm(
414        &self,
415        custom_account: Option<crate::crypto::vodozemac::olm::Account>,
416    ) -> Result<()> {
417        tracing::debug!("regenerating OlmMachine");
418        let session_meta = self.session_meta().ok_or(Error::OlmError(OlmError::MissingSession))?;
419
420        // Recreate the `OlmMachine` and wipe the in-memory cache in the store
421        // because we suspect it has stale data.
422        let builder = OlmMachineBuilder::new(&session_meta.user_id, &session_meta.device_id)
423            .with_crypto_store(self.crypto_store.clone())
424            .with_custom_account(custom_account);
425
426        #[cfg(feature = "experimental-x509-identity-verification")]
427        let builder = builder
428            .with_x509_verifier(self.x509_verifier.clone())
429            .with_x509_signer(self.x509_signer.clone());
430
431        let olm_machine = builder.build().await.map_err(OlmError::from)?;
432
433        *self.olm_machine.write().await = Some(olm_machine);
434        Ok(())
435    }
436
437    /// Get the current, if any, sync token of the client.
438    /// This will be None if the client didn't sync at least once.
439    pub async fn sync_token(&self) -> Option<String> {
440        self.state_store.sync_token.read().await.clone()
441    }
442
443    /// User has knocked on a room.
444    ///
445    /// Update the internal and cached state accordingly. Return the final Room.
446    pub async fn room_knocked(&self, room_id: &RoomId) -> Result<Room> {
447        let room = self.state_store.get_or_create_room(room_id, RoomState::Knocked);
448
449        if room.state() != RoomState::Knocked {
450            let store_guard = self.state_store.lock().lock().await;
451
452            // We are no longer joined to the room, so the invite acceptance details are no
453            // longer relevant.
454            #[cfg(feature = "e2e-encryption")]
455            if let Some(olm_machine) = self.olm_machine().await.as_ref() {
456                olm_machine.store().clear_room_pending_key_bundle(room_id).await?
457            }
458
459            room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
460                info.mark_as_knocked();
461                info.mark_state_partially_synced();
462                info.mark_members_missing(); // the own member event changed
463                (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
464            })
465            .await?;
466        }
467
468        Ok(room)
469    }
470
471    /// The user has joined a room using this specific client.
472    ///
473    /// This method should be called if the user accepts an invite or if they
474    /// join a public room.
475    ///
476    /// The method will create a [`Room`] object if one does not exist yet and
477    /// set the state of the [`Room`] to [`RoomState::Joined`]. The [`Room`]
478    /// object will be persisted in the cache. Please note that the [`Room`]
479    /// will be a stub until a sync has been received with the full room
480    /// state using [`BaseClient::receive_sync_response`].
481    ///
482    /// Update the internal and cached state accordingly. Return the final Room.
483    ///
484    /// # Arguments
485    ///
486    /// * `room_id` - The unique ID identifying the joined room.
487    /// * `inviter` - When joining this room in response to an invitation, the
488    ///   inviter should be recorded before sending the join request to the
489    ///   server. Providing the inviter here ensures that the
490    ///   [`RoomPendingKeyBundleDetails`] are stored for this room.
491    ///
492    /// # Examples
493    ///
494    /// ```rust
495    /// # use matrix_sdk_base::{BaseClient, store::StoreConfig, RoomState, ThreadingSupport, DmRoomDefinition};
496    /// # use ruma::{OwnedRoomId, OwnedUserId, RoomId};
497    /// use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
498    /// # async {
499    /// # let client = BaseClient::new(
500    ///     StoreConfig::new(CrossProcessLockConfig::multi_process("example")),
501    ///     ThreadingSupport::Disabled,
502    ///     DmRoomDefinition::default()
503    /// );
504    /// # async fn send_join_request() -> anyhow::Result<OwnedRoomId> { todo!() }
505    /// # async fn maybe_get_inviter(room_id: &RoomId) -> anyhow::Result<Option<OwnedUserId>> { todo!() }
506    /// # let room_id: &RoomId = todo!();
507    /// let maybe_inviter = maybe_get_inviter(room_id).await?;
508    /// let room_id = send_join_request().await?;
509    /// let room = client.room_joined(&room_id, maybe_inviter).await?;
510    ///
511    /// assert_eq!(room.state(), RoomState::Joined);
512    /// # matrix_sdk_test::TestResult::Ok(()) };
513    /// ```
514    pub async fn room_joined(
515        &self,
516        room_id: &RoomId,
517        inviter: Option<OwnedUserId>,
518    ) -> Result<Room> {
519        let room = self.state_store.get_or_create_room(room_id, RoomState::Joined);
520
521        // If the state isn't `RoomState::Joined` then this means that we knew about
522        // this room before. Let's modify the existing state now.
523        if room.state() != RoomState::Joined {
524            let store_guard = self.state_store_lock().lock().await;
525
526            #[cfg(feature = "e2e-encryption")]
527            {
528                // If our previous state was an invite and we're now in the joined state, this
529                // means that the user has explicitly accepted an invite. Let's
530                // remember some details about the invite.
531                //
532                // This is somewhat of a workaround for our lack of cryptographic membership.
533                // Later on we will decide if historic room keys should be accepted
534                // based on this info. If a user has accepted an invite and we receive a room
535                // key bundle shortly after, we might accept it. If we don't do
536                // this, the homeserver could trick us into accepting any historic room key
537                // bundle.
538                let previous_state = room.state();
539                if previous_state == RoomState::Invited
540                    && let Some(inviter) = inviter
541                    && let Some(olm_machine) = self.olm_machine().await.as_ref()
542                {
543                    olm_machine.store().store_room_pending_key_bundle(room_id, &inviter).await?
544                }
545            }
546            #[cfg(not(feature = "e2e-encryption"))]
547            {
548                // suppress unused argument warning
549                let _ = inviter;
550            }
551
552            room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
553                info.mark_as_joined();
554                info.mark_state_partially_synced();
555                info.mark_members_missing(); // the own member event changed
556                (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
557            })
558            .await?;
559        }
560
561        Ok(room)
562    }
563
564    /// User has left a room.
565    ///
566    /// Update the internal and cached state accordingly.
567    pub async fn room_left(&self, room_id: &RoomId) -> Result<()> {
568        let room = self.state_store.get_or_create_room(room_id, RoomState::Left);
569
570        if room.state() != RoomState::Left {
571            let store_guard = self.state_store.lock().lock().await;
572
573            // We are no longer joined to the room, so the invite acceptance details are no
574            // longer relevant.
575            #[cfg(feature = "e2e-encryption")]
576            if let Some(olm_machine) = self.olm_machine().await.as_ref() {
577                olm_machine.store().clear_room_pending_key_bundle(room_id).await?
578            }
579
580            room.update_and_save_room_info_with_store_guard(&store_guard, |mut info| {
581                info.mark_as_left();
582                info.mark_state_partially_synced();
583                info.mark_members_missing(); // the own member event changed
584                (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
585            })
586            .await?;
587        }
588
589        Ok(())
590    }
591
592    /// Get a lock to the state store, with an exclusive access.
593    ///
594    /// It doesn't give an access to the state store itself. It's rather a lock
595    /// to synchronise all accesses to the state store.
596    pub fn state_store_lock(&self) -> &Mutex<()> {
597        self.state_store.lock()
598    }
599
600    /// Receive a response from a sync call.
601    ///
602    /// # Arguments
603    ///
604    /// * `response` - The response that we received after a successful sync.
605    #[instrument(skip_all)]
606    pub async fn receive_sync_response(
607        &self,
608        response: api::sync::sync_events::v3::Response,
609    ) -> Result<SyncResponse> {
610        self.receive_sync_response_with_requested_required_states(
611            response,
612            &RequestedRequiredStates::default(),
613        )
614        .await
615    }
616
617    /// Receive a response from a sync call, with the requested required state
618    /// events.
619    ///
620    /// # Arguments
621    ///
622    /// * `response` - The response that we received after a successful sync.
623    /// * `requested_required_states` - The requested required state events.
624    pub async fn receive_sync_response_with_requested_required_states(
625        &self,
626        response: api::sync::sync_events::v3::Response,
627        requested_required_states: &RequestedRequiredStates,
628    ) -> Result<SyncResponse> {
629        // The server might respond multiple times with the same sync token, in
630        // that case we already received this response and there's nothing to
631        // do.
632        if self.state_store.sync_token.read().await.as_ref() == Some(&response.next_batch) {
633            info!("Got the same sync response twice");
634            return Ok(SyncResponse::default());
635        }
636
637        let now = if enabled!(Level::INFO) { Some(Instant::now()) } else { None };
638
639        // Acquire the state store lock and hold on to it while processing
640        // the sync response below.
641        let state_store_guard = self.state_store_lock().lock().await;
642
643        let user_id = self
644            .session_meta()
645            .expect("Sync shouldn't run without an authenticated user")
646            .user_id
647            .to_owned();
648
649        #[cfg(feature = "e2e-encryption")]
650        let olm_machine = self.olm_machine().await;
651
652        let mut context = Context::new(StateChanges::new(response.next_batch.clone()));
653
654        #[cfg(feature = "e2e-encryption")]
655        let processors::e2ee::to_device::Output { processed_to_device_events: to_device } =
656            processors::e2ee::to_device::from_sync_v2(
657                &response,
658                olm_machine.as_ref(),
659                &self.decryption_settings,
660            )
661            .await?;
662
663        #[cfg(not(feature = "e2e-encryption"))]
664        let to_device = response
665            .to_device
666            .events
667            .into_iter()
668            .map(|raw| {
669                use matrix_sdk_common::deserialized_responses::{
670                    ProcessedToDeviceEvent, ToDeviceUnableToDecryptInfo,
671                    ToDeviceUnableToDecryptReason,
672                };
673
674                if let Ok(Some(event_type)) = raw.get_field::<String>("type") {
675                    if event_type == "m.room.encrypted" {
676                        ProcessedToDeviceEvent::UnableToDecrypt {
677                            encrypted_event: raw,
678                            utd_info: ToDeviceUnableToDecryptInfo {
679                                reason: ToDeviceUnableToDecryptReason::EncryptionIsDisabled,
680                            },
681                        }
682                    } else {
683                        ProcessedToDeviceEvent::PlainText(raw)
684                    }
685                } else {
686                    // Exclude events with no type
687                    ProcessedToDeviceEvent::Invalid(raw)
688                }
689            })
690            .collect();
691
692        let mut ambiguity_cache = AmbiguityCache::new(self.state_store.inner.clone());
693        let mut avatar_cache = AvatarCache::new(self.state_store.inner.clone());
694
695        let global_account_data_processor =
696            processors::account_data::global(&response.account_data.events);
697
698        let push_rules = self.get_push_rules(&global_account_data_processor).await?;
699
700        let mut room_updates = RoomUpdates::default();
701        let mut notifications = Default::default();
702
703        let mut updated_members_in_room: BTreeMap<OwnedRoomId, BTreeSet<OwnedUserId>> =
704            BTreeMap::new();
705
706        #[cfg(feature = "e2e-encryption")]
707        let e2ee_context = processors::e2ee::E2EE::new(
708            olm_machine.as_ref(),
709            &self.decryption_settings,
710            self.handle_verification_events,
711        );
712
713        for (room_id, joined_room) in response.rooms.join {
714            let joined_room_update = processors::room::sync_v2::update_joined_room(
715                &mut context,
716                processors::room::RoomCreationData::new(
717                    &room_id,
718                    requested_required_states,
719                    &mut ambiguity_cache,
720                    &mut avatar_cache,
721                ),
722                joined_room,
723                &mut updated_members_in_room,
724                processors::notification::Notification::new(
725                    &push_rules,
726                    &mut notifications,
727                    &self.state_store,
728                ),
729                #[cfg(feature = "e2e-encryption")]
730                &e2ee_context,
731            )
732            .await?;
733
734            room_updates.joined.insert(room_id, joined_room_update);
735        }
736
737        for (room_id, left_room) in response.rooms.leave {
738            let left_room_update = processors::room::sync_v2::update_left_room(
739                &mut context,
740                processors::room::RoomCreationData::new(
741                    &room_id,
742                    requested_required_states,
743                    &mut ambiguity_cache,
744                    &mut avatar_cache,
745                ),
746                left_room,
747                processors::notification::Notification::new(
748                    &push_rules,
749                    &mut notifications,
750                    &self.state_store,
751                ),
752                #[cfg(feature = "e2e-encryption")]
753                &e2ee_context,
754            )
755            .await?;
756
757            room_updates.left.insert(room_id, left_room_update);
758        }
759
760        for (room_id, invited_room) in response.rooms.invite {
761            let invited_room_update = processors::room::sync_v2::update_invited_room(
762                &mut context,
763                &room_id,
764                &user_id,
765                invited_room,
766                processors::notification::Notification::new(
767                    &push_rules,
768                    &mut notifications,
769                    &self.state_store,
770                ),
771                #[cfg(feature = "e2e-encryption")]
772                &e2ee_context,
773            )
774            .await?;
775
776            room_updates.invited.insert(room_id, invited_room_update);
777        }
778
779        for (room_id, knocked_room) in response.rooms.knock {
780            let knocked_room_update = processors::room::sync_v2::update_knocked_room(
781                &mut context,
782                &room_id,
783                &user_id,
784                knocked_room,
785                processors::notification::Notification::new(
786                    &push_rules,
787                    &mut notifications,
788                    &self.state_store,
789                ),
790                #[cfg(feature = "e2e-encryption")]
791                &e2ee_context,
792            )
793            .await?;
794
795            room_updates.knocked.insert(room_id, knocked_room_update);
796        }
797
798        global_account_data_processor.apply(&mut context, &self.state_store).await;
799
800        context.state_changes.presence = response
801            .presence
802            .events
803            .iter()
804            .filter_map(|e| {
805                let event = e.deserialize().ok()?;
806                Some((event.sender, e.clone()))
807            })
808            .collect();
809
810        context.state_changes.ambiguity_maps = ambiguity_cache.cache;
811
812        processors::changes::save_and_apply(
813            context,
814            &self.state_store,
815            &state_store_guard,
816            &self.ignore_user_list_changes,
817            Some(response.next_batch.clone()),
818        )
819        .await?;
820
821        let mut context = Context::default();
822
823        // Now that all the rooms information have been saved, update the display name
824        // of the updated rooms (which relies on information stored in the database).
825        processors::room::display_name::update_for_rooms(
826            &mut context,
827            &room_updates,
828            &self.state_store,
829        )
830        .await;
831
832        // Save the new display name updates if any.
833        processors::changes::save_only(context, &self.state_store, &state_store_guard).await?;
834
835        for (room_id, member_ids) in updated_members_in_room {
836            if let Some(room) = self.get_room(&room_id) {
837                let _ =
838                    room.room_member_updates_sender.send(RoomMembersUpdate::Partial(member_ids));
839            }
840        }
841
842        // Release the state store lock
843        drop(state_store_guard);
844
845        if enabled!(Level::INFO) {
846            info!("Processed a sync response in {:?}", now.map(|now| now.elapsed()));
847        }
848
849        let response = SyncResponse {
850            rooms: room_updates,
851            presence: response.presence.events,
852            account_data: response.account_data.events,
853            to_device,
854            notifications,
855        };
856
857        Ok(response)
858    }
859
860    /// Receive a get member events response and convert it to a deserialized
861    /// `MembersResponse`
862    ///
863    /// This client-server request must be made without filters to make sure all
864    /// members are received. Otherwise, an error is returned.
865    ///
866    /// # Arguments
867    ///
868    /// * `room_id` - The room id this response belongs to.
869    ///
870    /// * `response` - The raw response that was received from the server.
871    #[instrument(skip_all, fields(?room_id))]
872    pub async fn receive_all_members(
873        &self,
874        room_id: &RoomId,
875        request: &api::membership::get_member_events::v3::Request,
876        response: &api::membership::get_member_events::v3::Response,
877    ) -> Result<()> {
878        if request.membership.is_some() || request.not_membership.is_some() || request.at.is_some()
879        {
880            // This function assumes all members are loaded at once to optimise how display
881            // name disambiguation works. Using it with partial member list results
882            // would produce incorrect disambiguated display name entries
883            return Err(Error::InvalidReceiveMembersParameters);
884        }
885
886        let Some(room) = self.state_store.room(room_id) else {
887            // The room is unknown to us: leave early.
888            return Ok(());
889        };
890
891        let mut chunk = Vec::with_capacity(response.chunk.len());
892        let mut context = Context::default();
893
894        #[cfg(feature = "e2e-encryption")]
895        let mut user_ids = BTreeSet::new();
896
897        let mut ambiguity_map: HashMap<DisplayName, BTreeSet<OwnedUserId>> = Default::default();
898
899        for raw_event in &response.chunk {
900            let member = match raw_event.deserialize() {
901                Ok(ev) => ev,
902                Err(e) => {
903                    let event_id: Option<String> = raw_event.get_field("event_id").ok().flatten();
904                    debug!(event_id, "Failed to deserialize member event: {e}");
905                    continue;
906                }
907            };
908
909            // TODO: All the actions in this loop used to be done only when the membership
910            // event was not in the store before. This was changed with the new room API,
911            // because e.g. leaving a room makes members events outdated and they need to be
912            // fetched by `members`. Therefore, they need to be overwritten here, even
913            // if they exist.
914            // However, this makes a new problem occur where setting the member events here
915            // potentially races with the sync.
916            // See <https://github.com/matrix-org/matrix-rust-sdk/issues/1205>.
917
918            #[cfg(feature = "e2e-encryption")]
919            match member.membership() {
920                MembershipState::Join | MembershipState::Invite => {
921                    user_ids.insert(member.state_key().to_owned());
922                }
923                _ => (),
924            }
925
926            if let StateEvent::Original(e) = &member
927                && let Some(d) = &e.content.displayname
928            {
929                let display_name = DisplayName::new(d);
930                ambiguity_map.entry(display_name).or_default().insert(member.state_key().clone());
931            }
932
933            let sync_member: SyncRoomMemberEvent = member.clone().into();
934            processors::profiles::upsert_or_delete(&mut context, room_id, &sync_member);
935
936            context
937                .state_changes
938                .state
939                .entry(room_id.to_owned())
940                .or_default()
941                .entry(member.event_type())
942                .or_default()
943                .insert(member.state_key().to_string(), raw_event.clone().cast());
944            chunk.push(member);
945        }
946
947        #[cfg(feature = "e2e-encryption")]
948        processors::e2ee::tracked_users::update(
949            self.olm_machine().await.as_ref(),
950            room.encryption_state(),
951            &user_ids,
952        )
953        .await?;
954
955        context.state_changes.ambiguity_maps.insert(room_id.to_owned(), ambiguity_map);
956
957        {
958            let state_store_guard = self.state_store_lock().lock().await;
959
960            let mut room_info = room.clone_info();
961            room_info.mark_members_synced();
962            context.state_changes.add_room(room_info);
963
964            processors::changes::save_and_apply(
965                context,
966                &self.state_store,
967                &state_store_guard,
968                &self.ignore_user_list_changes,
969                None,
970            )
971            .await?;
972        }
973
974        let _ = room.room_member_updates_sender.send(RoomMembersUpdate::FullReload);
975
976        #[cfg(feature = "e2e-encryption")]
977        if let Some(olm) = self.olm_machine().await.as_ref() {
978            // With the introduction of MSC4268, it is no longer sufficient to check for
979            // changes to session recipients when we send a message, since we may miss
980            // join/leave pairs in our view of the room state. Instead, we should rotate
981            // the room key whenever we fully reload the member list as a precaution.
982            tracing::debug!("Rotating room key due to full member list reload");
983            if let Err(e) = olm.discard_room_key(room_id).await {
984                tracing::warn!("Error discarding room key: {e:?}");
985            }
986        }
987
988        Ok(())
989    }
990
991    /// Receive a successful filter upload response, the filter id will be
992    /// stored under the given name in the store.
993    ///
994    /// The filter id can later be retrieved with the [`get_filter`] method.
995    ///
996    ///
997    /// # Arguments
998    ///
999    /// * `filter_name` - The name that should be used to persist the filter id
1000    ///   in the store.
1001    ///
1002    /// * `response` - The successful filter upload response containing the
1003    ///   filter id.
1004    ///
1005    /// [`get_filter`]: #method.get_filter
1006    pub async fn receive_filter_upload(
1007        &self,
1008        filter_name: &str,
1009        response: &api::filter::create_filter::v3::Response,
1010    ) -> Result<()> {
1011        Ok(self
1012            .state_store
1013            .set_kv_data(
1014                StateStoreDataKey::Filter(filter_name),
1015                StateStoreDataValue::Filter(response.filter_id.clone()),
1016            )
1017            .await?)
1018    }
1019
1020    /// Get the filter id of a previously uploaded filter.
1021    ///
1022    /// *Note*: A filter will first need to be uploaded and persisted using
1023    /// [`receive_filter_upload`].
1024    ///
1025    /// # Arguments
1026    ///
1027    /// * `filter_name` - The name of the filter that was previously used to
1028    ///   persist the filter.
1029    ///
1030    /// [`receive_filter_upload`]: #method.receive_filter_upload
1031    pub async fn get_filter(&self, filter_name: &str) -> StoreResult<Option<String>> {
1032        let filter = self
1033            .state_store
1034            .get_kv_data(StateStoreDataKey::Filter(filter_name))
1035            .await?
1036            .map(|d| d.into_filter().expect("State store data not a filter"));
1037
1038        Ok(filter)
1039    }
1040
1041    /// Get a to-device request that will share a room key with users in a room.
1042    #[cfg(feature = "e2e-encryption")]
1043    pub async fn share_room_key(&self, room_id: &RoomId) -> Result<Vec<Arc<ToDeviceRequest>>> {
1044        match self.olm_machine().await.as_ref() {
1045            Some(o) => {
1046                let Some(room) = self.get_room(room_id) else {
1047                    return Err(Error::InsufficientData);
1048                };
1049
1050                let history_visibility = room.history_visibility_or_default();
1051                let Some(room_encryption_event) = room.encryption_settings() else {
1052                    return Err(Error::EncryptionNotEnabled);
1053                };
1054
1055                // Don't share the group session with members that are invited
1056                // if the history visibility is set to `Joined`
1057                let filter = if history_visibility == HistoryVisibility::Joined {
1058                    RoomMemberships::JOIN
1059                } else {
1060                    RoomMemberships::ACTIVE
1061                };
1062
1063                let members = self.state_store.get_user_ids(room_id, filter).await?;
1064
1065                let Some(settings) = EncryptionSettings::from_possibly_redacted(
1066                    room_encryption_event,
1067                    history_visibility,
1068                    self.room_key_recipient_strategy.clone(),
1069                ) else {
1070                    return Err(Error::EncryptionNotEnabled);
1071                };
1072
1073                Ok(o.share_room_key(room_id, members.iter().map(Deref::deref), settings).await?)
1074            }
1075            None => panic!("Olm machine wasn't started"),
1076        }
1077    }
1078
1079    /// Get the room with the given room id.
1080    ///
1081    /// # Arguments
1082    ///
1083    /// * `room_id` - The id of the room that should be fetched.
1084    pub fn get_room(&self, room_id: &RoomId) -> Option<Room> {
1085        self.state_store.room(room_id)
1086    }
1087
1088    /// Forget the room with the given room ID.
1089    ///
1090    /// The room will be dropped from the room list and the store.
1091    ///
1092    /// # Arguments
1093    ///
1094    /// * `room_id` - The id of the room that should be forgotten.
1095    pub async fn forget_room(&self, room_id: &RoomId) -> Result<()> {
1096        // Forget the room in the state store.
1097        self.state_store.forget_room(room_id).await?;
1098
1099        Ok(())
1100    }
1101
1102    /// Get the olm machine.
1103    #[cfg(feature = "e2e-encryption")]
1104    pub async fn olm_machine(&self) -> RwLockReadGuard<'_, Option<OlmMachine>> {
1105        self.olm_machine.read().await
1106    }
1107
1108    /// Get the push rules.
1109    ///
1110    /// Gets the push rules previously processed, otherwise get them from the
1111    /// store. As a fallback, uses [`Ruleset::server_default`] if the user
1112    /// is logged in.
1113    pub(crate) async fn get_push_rules(
1114        &self,
1115        global_account_data_processor: &processors::account_data::Global,
1116    ) -> Result<Ruleset> {
1117        let _timer = timer!(Level::TRACE, "get_push_rules");
1118        if let Some(event) = global_account_data_processor
1119            .push_rules()
1120            .and_then(|ev| ev.deserialize_as_unchecked::<PushRulesEvent>().ok())
1121        {
1122            Ok(event.content.global)
1123        } else if let Some(event) = self
1124            .state_store
1125            .get_account_data_event_static::<PushRulesEventContent>()
1126            .await?
1127            .and_then(|ev| ev.deserialize().ok())
1128        {
1129            Ok(event.content.global)
1130        } else if let Some(session_meta) = self.state_store.session_meta() {
1131            Ok(Ruleset::server_default(&session_meta.user_id))
1132        } else {
1133            Ok(Ruleset::new())
1134        }
1135    }
1136
1137    /// Returns a subscriber that publishes an event every time the ignore user
1138    /// list changes
1139    pub fn subscribe_to_ignore_user_list_changes(&self) -> Subscriber<Vec<String>> {
1140        self.ignore_user_list_changes.subscribe()
1141    }
1142
1143    /// Returns a new receiver that gets future room info notable updates.
1144    ///
1145    /// Learn more by reading the [`RoomInfoNotableUpdate`] type.
1146    pub fn room_info_notable_update_receiver(&self) -> broadcast::Receiver<RoomInfoNotableUpdate> {
1147        self.state_store.room_info_notable_update_sender.subscribe()
1148    }
1149
1150    /// Returns a receiver of the user IDs whose global profile changed during a
1151    /// sync. Consumers can use this as a trigger to e.g. merge any global
1152    /// fields into a user's room profile.
1153    ///
1154    /// Requires the Profiles sliding sync extension to be enabled.
1155    pub fn subscribe_to_global_profile_updates(
1156        &self,
1157    ) -> broadcast::Receiver<BTreeSet<OwnedUserId>> {
1158        self.global_profile_updates_sender.subscribe()
1159    }
1160
1161    /// Checks whether the provided `user_id` belongs to an ignored user.
1162    pub async fn is_user_ignored(&self, user_id: &UserId) -> bool {
1163        match self.state_store.get_account_data_event_static::<IgnoredUserListEventContent>().await
1164        {
1165            Ok(Some(raw_ignored_user_list)) => match raw_ignored_user_list.deserialize() {
1166                Ok(current_ignored_user_list) => {
1167                    current_ignored_user_list.content.ignored_users.contains_key(user_id)
1168                }
1169                Err(error) => {
1170                    warn!(?error, "Failed to deserialize the ignored user list event");
1171                    false
1172                }
1173            },
1174            Ok(None) => false,
1175            Err(error) => {
1176                warn!(?error, "Could not get the ignored user list from the state store");
1177                false
1178            }
1179        }
1180    }
1181
1182    /// Check the record of whether we are waiting for an [MSC4268] key bundle
1183    /// for the given room.
1184    ///
1185    /// [MSC4268]: https://github.com/matrix-org/matrix-spec-proposals/pull/4268
1186    #[cfg(feature = "e2e-encryption")]
1187    pub async fn get_pending_key_bundle_details_for_room(
1188        &self,
1189        room_id: &RoomId,
1190    ) -> Result<Option<RoomPendingKeyBundleDetails>> {
1191        let result = match self.olm_machine().await.as_ref() {
1192            Some(machine) => {
1193                machine.store().get_pending_key_bundle_details_for_room(room_id).await?
1194            }
1195            None => None,
1196        };
1197        Ok(result)
1198    }
1199
1200    /// Close all stores, releasing database connections and file locks.
1201    ///
1202    /// In-flight operations will complete before this returns.
1203    pub async fn close_stores(&self) -> Result<()> {
1204        self.state_store.close().await?;
1205        self.event_cache_store.close().await.map_err(Error::EventCacheStore)?;
1206        self.media_store.close().await.map_err(Error::MediaStore)?;
1207
1208        #[cfg(feature = "e2e-encryption")]
1209        self.crypto_store.close().await.map_err(Error::CryptoStore)?;
1210
1211        Ok(())
1212    }
1213
1214    /// Reopen all stores after a close, re-opening database connections.
1215    pub async fn reopen_stores(&self) -> Result<()> {
1216        #[cfg(feature = "e2e-encryption")]
1217        self.crypto_store.reopen().await.map_err(Error::CryptoStore)?;
1218
1219        self.media_store.reopen().await.map_err(Error::MediaStore)?;
1220        self.event_cache_store.reopen().await.map_err(Error::EventCacheStore)?;
1221        self.state_store.reopen().await?;
1222
1223        Ok(())
1224    }
1225}
1226
1227/// Represent the `required_state` values sent by a sync request.
1228///
1229/// This is useful to track what state events have been requested when handling
1230/// a response.
1231///
1232/// For example, if a sync requests the `m.room.encryption` state event, and the
1233/// server replies with nothing, if means the room **is not** encrypted. Without
1234/// knowing which state event was required by the sync, it is impossible to
1235/// interpret the absence of state event from the server as _the room's
1236/// encryption state is **not encrypted**_ or _the room's encryption state is
1237/// **unknown**_.
1238#[derive(Debug, Default)]
1239pub struct RequestedRequiredStates {
1240    default: Vec<(StateEventType, String)>,
1241    for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1242}
1243
1244impl RequestedRequiredStates {
1245    /// Create a new `RequestedRequiredStates`.
1246    ///
1247    /// `default` represents the `required_state` value for all rooms.
1248    /// `for_rooms` is the `required_state` per room.
1249    pub fn new(
1250        default: Vec<(StateEventType, String)>,
1251        for_rooms: HashMap<OwnedRoomId, Vec<(StateEventType, String)>>,
1252    ) -> Self {
1253        Self { default, for_rooms }
1254    }
1255
1256    /// Get the `required_state` value for a specific room.
1257    pub fn for_room(&self, room_id: &RoomId) -> &[(StateEventType, String)] {
1258        self.for_rooms.get(room_id).unwrap_or(&self.default)
1259    }
1260}
1261
1262impl From<&v5::Request> for RequestedRequiredStates {
1263    fn from(request: &v5::Request) -> Self {
1264        // The following information is missing in the MSC4186 at the time of writing
1265        // (2025-03-12) but: the `required_state`s from all lists and from all room
1266        // subscriptions are combined by doing an union.
1267        //
1268        // Thus, we can do the same here, put the union in `default` and keep
1269        // `for_rooms` empty. The `Self::for_room` will automatically do the fallback.
1270        let mut default = BTreeSet::new();
1271
1272        for list in request.lists.values() {
1273            default.extend(BTreeSet::from_iter(list.room_details.required_state.iter().cloned()));
1274        }
1275
1276        for room_subscription in request.room_subscriptions.values() {
1277            default.extend(BTreeSet::from_iter(room_subscription.required_state.iter().cloned()));
1278        }
1279
1280        Self { default: default.into_iter().collect(), for_rooms: HashMap::new() }
1281    }
1282}
1283
1284/// An enum that defines what the [`BaseClient`] should consider a DM room.
1285#[derive(Debug, Clone, Default)]
1286#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
1287pub enum DmRoomDefinition {
1288    /// Standard Matrix spec definition: a room linked to a user in an
1289    /// `m.direct` event.
1290    #[default]
1291    MatrixSpec,
1292    /// A room that is direct, as per the spec but also contains at most 2
1293    /// active members.
1294    TwoMembers,
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299    use std::collections::HashMap;
1300
1301    use assert_matches2::assert_let;
1302    #[cfg(feature = "e2e-encryption")]
1303    use assert_matches2::assert_matches;
1304    use futures_util::FutureExt as _;
1305    use matrix_sdk_common::cross_process_lock::CrossProcessLockConfig;
1306    use matrix_sdk_test::{
1307        BOB, InvitedRoomBuilder, LeftRoomBuilder, SyncResponseBuilder, async_test,
1308        event_factory::EventFactory, ruma_response_from_json,
1309    };
1310    #[cfg(feature = "unstable-msc4426")]
1311    use ruma::profile::{
1312        ProfileFieldValue, StatusProfileField, UserProfileChanges, UserProfileUpdate,
1313    };
1314    use ruma::{
1315        api::client::{self as api, sync::sync_events::v5},
1316        event_id,
1317        events::{StateEventType, room::member::MembershipState},
1318        room_id,
1319        serde::Raw,
1320        user_id,
1321    };
1322    use serde_json::{json, value::to_raw_value};
1323
1324    use super::{BaseClient, RequestedRequiredStates};
1325    use crate::{
1326        DmRoomDefinition, RoomDisplayName, RoomState, SessionMeta,
1327        client::ThreadingSupport,
1328        store::{RoomLoadSettings, StateStoreExt, StoreConfig},
1329        test_utils::logged_in_base_client,
1330    };
1331    #[cfg(feature = "unstable-msc4426")]
1332    use crate::{RoomMemberships, store::StateChanges};
1333
1334    #[test]
1335    fn test_requested_required_states() {
1336        let room_id_0 = room_id!("!r0");
1337        let room_id_1 = room_id!("!r1");
1338
1339        let requested_required_states = RequestedRequiredStates::new(
1340            vec![(StateEventType::RoomAvatar, "".to_owned())],
1341            HashMap::from([(
1342                room_id_0.to_owned(),
1343                vec![
1344                    (StateEventType::RoomMember, "foo".to_owned()),
1345                    (StateEventType::RoomEncryption, "".to_owned()),
1346                ],
1347            )]),
1348        );
1349
1350        // A special set of state events exists for `room_id_0`.
1351        assert_eq!(
1352            requested_required_states.for_room(room_id_0),
1353            &[
1354                (StateEventType::RoomMember, "foo".to_owned()),
1355                (StateEventType::RoomEncryption, "".to_owned()),
1356            ]
1357        );
1358
1359        // No special list for `room_id_1`, it should return the defaults.
1360        assert_eq!(
1361            requested_required_states.for_room(room_id_1),
1362            &[(StateEventType::RoomAvatar, "".to_owned()),]
1363        );
1364    }
1365
1366    #[test]
1367    fn test_requested_required_states_from_sync_v5_request() {
1368        let room_id_0 = room_id!("!r0");
1369        let room_id_1 = room_id!("!r1");
1370
1371        // Empty request.
1372        let mut request = v5::Request::new();
1373
1374        {
1375            let requested_required_states = RequestedRequiredStates::from(&request);
1376
1377            assert!(requested_required_states.default.is_empty());
1378            assert!(requested_required_states.for_rooms.is_empty());
1379        }
1380
1381        // One list.
1382        request.lists.insert("foo".to_owned(), {
1383            let mut list = v5::request::List::default();
1384            list.room_details.required_state = vec![
1385                (StateEventType::RoomAvatar, "".to_owned()),
1386                (StateEventType::RoomEncryption, "".to_owned()),
1387            ];
1388
1389            list
1390        });
1391
1392        {
1393            let requested_required_states = RequestedRequiredStates::from(&request);
1394
1395            assert_eq!(
1396                requested_required_states.default,
1397                &[
1398                    (StateEventType::RoomAvatar, "".to_owned()),
1399                    (StateEventType::RoomEncryption, "".to_owned())
1400                ]
1401            );
1402            assert!(requested_required_states.for_rooms.is_empty());
1403        }
1404
1405        // Two lists.
1406        request.lists.insert("bar".to_owned(), {
1407            let mut list = v5::request::List::default();
1408            list.room_details.required_state = vec![
1409                (StateEventType::RoomEncryption, "".to_owned()),
1410                (StateEventType::RoomName, "".to_owned()),
1411            ];
1412
1413            list
1414        });
1415
1416        {
1417            let requested_required_states = RequestedRequiredStates::from(&request);
1418
1419            // Union of the state events.
1420            assert_eq!(
1421                requested_required_states.default,
1422                &[
1423                    (StateEventType::RoomAvatar, "".to_owned()),
1424                    (StateEventType::RoomEncryption, "".to_owned()),
1425                    (StateEventType::RoomName, "".to_owned()),
1426                ]
1427            );
1428            assert!(requested_required_states.for_rooms.is_empty());
1429        }
1430
1431        // One room subscription.
1432        request.room_subscriptions.insert(room_id_0.to_owned(), {
1433            let mut room_subscription = v5::request::RoomSubscription::default();
1434
1435            room_subscription.required_state = vec![
1436                (StateEventType::RoomJoinRules, "".to_owned()),
1437                (StateEventType::RoomEncryption, "".to_owned()),
1438            ];
1439
1440            room_subscription
1441        });
1442
1443        {
1444            let requested_required_states = RequestedRequiredStates::from(&request);
1445
1446            // Union of state events, all in `default`, still nothing in `for_rooms`.
1447            assert_eq!(
1448                requested_required_states.default,
1449                &[
1450                    (StateEventType::RoomAvatar, "".to_owned()),
1451                    (StateEventType::RoomEncryption, "".to_owned()),
1452                    (StateEventType::RoomJoinRules, "".to_owned()),
1453                    (StateEventType::RoomName, "".to_owned()),
1454                ]
1455            );
1456            assert!(requested_required_states.for_rooms.is_empty());
1457        }
1458
1459        // Two room subscriptions.
1460        request.room_subscriptions.insert(room_id_1.to_owned(), {
1461            let mut room_subscription = v5::request::RoomSubscription::default();
1462
1463            room_subscription.required_state = vec![
1464                (StateEventType::RoomName, "".to_owned()),
1465                (StateEventType::RoomTopic, "".to_owned()),
1466            ];
1467
1468            room_subscription
1469        });
1470
1471        {
1472            let requested_required_states = RequestedRequiredStates::from(&request);
1473
1474            // Union of state events, all in `default`, still nothing in `for_rooms`.
1475            assert_eq!(
1476                requested_required_states.default,
1477                &[
1478                    (StateEventType::RoomAvatar, "".to_owned()),
1479                    (StateEventType::RoomEncryption, "".to_owned()),
1480                    (StateEventType::RoomJoinRules, "".to_owned()),
1481                    (StateEventType::RoomName, "".to_owned()),
1482                    (StateEventType::RoomTopic, "".to_owned()),
1483                ]
1484            );
1485        }
1486    }
1487
1488    #[async_test]
1489    async fn test_invite_after_leaving() {
1490        let user_id = user_id!("@alice:example.org");
1491        let room_id = room_id!("!test:example.org");
1492
1493        let client = logged_in_base_client(Some(user_id)).await;
1494        let f = EventFactory::new();
1495
1496        let mut sync_builder = SyncResponseBuilder::new();
1497
1498        let response = sync_builder
1499            .add_left_room(
1500                LeftRoomBuilder::new(room_id).add_timeline_event(
1501                    EventFactory::new()
1502                        .member(user_id)
1503                        .membership(MembershipState::Leave)
1504                        .display_name("Alice")
1505                        .event_id(event_id!("$994173582443PhrSn:example.org")),
1506                ),
1507            )
1508            .build_sync_response();
1509        client.receive_sync_response(response).await.unwrap();
1510        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Left);
1511
1512        let response = sync_builder
1513            .add_invited_room(
1514                InvitedRoomBuilder::new(room_id).add_state_event(
1515                    f.member(user_id)
1516                        .sender(user_id!("@example:example.org"))
1517                        .membership(MembershipState::Invite)
1518                        .display_name("Alice"),
1519                ),
1520            )
1521            .build_sync_response();
1522        client.receive_sync_response(response).await.unwrap();
1523        assert_eq!(client.get_room(room_id).unwrap().state(), RoomState::Invited);
1524    }
1525
1526    #[async_test]
1527    async fn test_invite_displayname() {
1528        let user_id = user_id!("@alice:example.org");
1529        let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1530
1531        let client = logged_in_base_client(Some(user_id)).await;
1532
1533        let response = ruma_response_from_json(&json!({
1534            "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1535            "device_one_time_keys_count": {
1536                "signed_curve25519": 50u64
1537            },
1538            "device_unused_fallback_key_types": [
1539                "signed_curve25519"
1540            ],
1541            "rooms": {
1542                "invite": {
1543                    "!ithpyNKDtmhneaTQja:example.org": {
1544                        "invite_state": {
1545                            "events": [
1546                                {
1547                                    "content": {
1548                                        "creator": "@test:example.org",
1549                                        "room_version": "9"
1550                                    },
1551                                    "sender": "@test:example.org",
1552                                    "state_key": "",
1553                                    "type": "m.room.create"
1554                                },
1555                                {
1556                                    "content": {
1557                                        "join_rule": "invite"
1558                                    },
1559                                    "sender": "@test:example.org",
1560                                    "state_key": "",
1561                                    "type": "m.room.join_rules"
1562                                },
1563                                {
1564                                    "content": {
1565                                        "algorithm": "m.megolm.v1.aes-sha2"
1566                                    },
1567                                    "sender": "@test:example.org",
1568                                    "state_key": "",
1569                                    "type": "m.room.encryption"
1570                                },
1571                                {
1572                                    "content": {
1573                                        "avatar_url": "mxc://example.org/dcBBDwuWEUrjfrOchvkirUST",
1574                                        "displayname": "Kyra",
1575                                        "membership": "join"
1576                                    },
1577                                    "sender": "@test:example.org",
1578                                    "state_key": "@test:example.org",
1579                                    "type": "m.room.member"
1580                                },
1581                                {
1582                                    "content": {
1583                                        "avatar_url": "mxc://example.org/ABFEXSDrESxovWwEnCYdNcHT",
1584                                        "displayname": "alice",
1585                                        "is_direct": true,
1586                                        "membership": "invite"
1587                                    },
1588                                    "origin_server_ts": 1650878657984u64,
1589                                    "sender": "@test:example.org",
1590                                    "state_key": "@alice:example.org",
1591                                    "type": "m.room.member",
1592                                    "unsigned": {
1593                                        "age": 14u64
1594                                    },
1595                                    "event_id": "$fLDqltg9Puj-kWItLSFVHPGN4YkgpYQf2qImPzdmgrE"
1596                                }
1597                            ]
1598                        }
1599                    }
1600                }
1601            }
1602        }));
1603
1604        client.receive_sync_response(response).await.unwrap();
1605
1606        let room = client.get_room(room_id).expect("Room not found");
1607        assert_eq!(room.state(), RoomState::Invited);
1608        assert_eq!(
1609            room.compute_display_name().await.expect("fetching display name failed").into_inner(),
1610            RoomDisplayName::Calculated("Kyra".to_owned())
1611        );
1612    }
1613
1614    #[async_test]
1615    async fn test_deserialization_failure() {
1616        let user_id = user_id!("@alice:example.org");
1617        let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1618
1619        let client = BaseClient::new(
1620            StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1621            ThreadingSupport::Disabled,
1622            DmRoomDefinition::default(),
1623        );
1624        client
1625            .activate(
1626                SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1627                RoomLoadSettings::default(),
1628                #[cfg(feature = "e2e-encryption")]
1629                None,
1630            )
1631            .await
1632            .unwrap();
1633
1634        let response = ruma_response_from_json(&json!({
1635            "next_batch": "asdkl;fjasdkl;fj;asdkl;f",
1636            "rooms": {
1637                "join": {
1638                    "!ithpyNKDtmhneaTQja:example.org": {
1639                        "state": {
1640                            "events": [
1641                                {
1642                                    "invalid": "invalid",
1643                                },
1644                                {
1645                                    "content": {
1646                                        "name": "The room name"
1647                                    },
1648                                    "event_id": "$143273582443PhrSn:example.org",
1649                                    "origin_server_ts": 1432735824653u64,
1650                                    "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
1651                                    "sender": "@example:example.org",
1652                                    "state_key": "",
1653                                    "type": "m.room.name",
1654                                    "unsigned": {
1655                                        "age": 1234
1656                                    }
1657                                },
1658                            ]
1659                        }
1660                    }
1661                }
1662            }
1663        }));
1664
1665        client.receive_sync_response(response).await.unwrap();
1666        client
1667            .state_store()
1668            .get_state_event_static::<ruma::events::room::name::RoomNameEventContent>(room_id)
1669            .await
1670            .expect("Failed to fetch state event")
1671            .expect("State event not found")
1672            .deserialize()
1673            .expect("Failed to deserialize state event");
1674    }
1675
1676    #[async_test]
1677    async fn test_invited_members_arent_ignored() {
1678        let user_id = user_id!("@alice:example.org");
1679        let inviter_user_id = user_id!("@bob:example.org");
1680        let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1681
1682        let client = BaseClient::new(
1683            StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1684            ThreadingSupport::Disabled,
1685            DmRoomDefinition::default(),
1686        );
1687        client
1688            .activate(
1689                SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1690                RoomLoadSettings::default(),
1691                #[cfg(feature = "e2e-encryption")]
1692                None,
1693            )
1694            .await
1695            .unwrap();
1696
1697        // Preamble: let the SDK know about the room.
1698        let mut sync_builder = SyncResponseBuilder::new();
1699        let response = sync_builder
1700            .add_joined_room(matrix_sdk_test::JoinedRoomBuilder::new(room_id))
1701            .build_sync_response();
1702        client.receive_sync_response(response).await.unwrap();
1703
1704        // When I process the result of a /members request that only contains an invited
1705        // member,
1706        let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1707
1708        let raw_member_event = json!({
1709            "content": {
1710                "avatar_url": "mxc://localhost/fewjilfewjil42",
1711                "displayname": "Invited Alice",
1712                "membership": "invite"
1713            },
1714            "event_id": "$151800140517rfvjc:localhost",
1715            "origin_server_ts": 151800140,
1716            "room_id": room_id,
1717            "sender": inviter_user_id,
1718            "state_key": user_id,
1719            "type": "m.room.member",
1720            "unsigned": {
1721                "age": 13374242,
1722            }
1723        });
1724        let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1725            to_raw_value(&raw_member_event).unwrap(),
1726        )]);
1727
1728        // It's correctly processed,
1729        client.receive_all_members(room_id, &request, &response).await.unwrap();
1730
1731        let room = client.get_room(room_id).unwrap();
1732
1733        // And I can get the invited member display name and avatar.
1734        let member = room.get_member(user_id).await.expect("ok").expect("exists");
1735
1736        assert_eq!(member.user_id(), user_id);
1737        assert_eq!(member.display_name().unwrap(), "Invited Alice");
1738        assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1739    }
1740
1741    #[async_test]
1742    async fn test_reinvited_members_get_a_display_name() {
1743        let user_id = user_id!("@alice:example.org");
1744        let inviter_user_id = user_id!("@bob:example.org");
1745        let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1746
1747        let client = BaseClient::new(
1748            StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1749            ThreadingSupport::Disabled,
1750            DmRoomDefinition::default(),
1751        );
1752        client
1753            .activate(
1754                SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1755                RoomLoadSettings::default(),
1756                #[cfg(feature = "e2e-encryption")]
1757                None,
1758            )
1759            .await
1760            .unwrap();
1761
1762        // Preamble: let the SDK know about the room, and that the invited user left it.
1763        let f = EventFactory::new().sender(user_id);
1764        let mut sync_builder = SyncResponseBuilder::new();
1765        let response = sync_builder
1766            .add_joined_room(
1767                matrix_sdk_test::JoinedRoomBuilder::new(room_id)
1768                    .add_state_event(f.member(user_id).leave()),
1769            )
1770            .build_sync_response();
1771        client.receive_sync_response(response).await.unwrap();
1772
1773        // Now, say that the user has been re-invited.
1774        let request = api::membership::get_member_events::v3::Request::new(room_id.to_owned());
1775
1776        let raw_member_event = json!({
1777            "content": {
1778                "avatar_url": "mxc://localhost/fewjilfewjil42",
1779                "displayname": "Invited Alice",
1780                "membership": "invite"
1781            },
1782            "event_id": "$151800140517rfvjc:localhost",
1783            "origin_server_ts": 151800140,
1784            "room_id": room_id,
1785            "sender": inviter_user_id,
1786            "state_key": user_id,
1787            "type": "m.room.member",
1788            "unsigned": {
1789                "age": 13374242,
1790            }
1791        });
1792        let response = api::membership::get_member_events::v3::Response::new(vec![Raw::from_json(
1793            to_raw_value(&raw_member_event).unwrap(),
1794        )]);
1795
1796        // It's correctly processed,
1797        client.receive_all_members(room_id, &request, &response).await.unwrap();
1798
1799        let room = client.get_room(room_id).unwrap();
1800
1801        // And I can get the invited member display name and avatar.
1802        let member = room.get_member(user_id).await.expect("ok").expect("exists");
1803
1804        assert_eq!(member.user_id(), user_id);
1805        assert_eq!(member.display_name().unwrap(), "Invited Alice");
1806        assert_eq!(member.avatar_url().unwrap().to_string(), "mxc://localhost/fewjilfewjil42");
1807    }
1808
1809    #[cfg(feature = "unstable-msc4426")]
1810    #[async_test]
1811    async fn test_room_member_carries_global_profile_status() {
1812        let user_id = user_id!("@alice:example.org");
1813        let room_id = room_id!("!ithpyNKDtmhneaTQja:example.org");
1814
1815        let client = BaseClient::new(
1816            StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1817            ThreadingSupport::Disabled,
1818            DmRoomDefinition::default(),
1819        );
1820        client
1821            .activate(
1822                SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1823                RoomLoadSettings::default(),
1824                #[cfg(feature = "e2e-encryption")]
1825                None,
1826            )
1827            .await
1828            .unwrap();
1829
1830        // Let the SDK know about the room, with the user as a joined member.
1831        let f = EventFactory::new().sender(user_id);
1832        let mut sync_builder = SyncResponseBuilder::new();
1833        let response = sync_builder
1834            .add_joined_room(
1835                matrix_sdk_test::JoinedRoomBuilder::new(room_id).add_state_event(f.member(user_id)),
1836            )
1837            .build_sync_response();
1838        client.receive_sync_response(response).await.unwrap();
1839
1840        let room = client.get_room(room_id).unwrap();
1841
1842        // Without a global profile, the member has no status.
1843        let member = room.get_member(user_id).await.expect("ok").expect("exists");
1844        assert!(member.status().is_none());
1845
1846        // Save a global profile carrying an `m.status` for the member.
1847        let mut changes = StateChanges::default();
1848        changes.global_profiles.insert(user_id.to_owned(), {
1849            let mut profile_changes = UserProfileChanges::new();
1850            profile_changes.insert_updated_value(ProfileFieldValue::Status(
1851                StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
1852            ));
1853            UserProfileUpdate::Updated(profile_changes)
1854        });
1855        client.state_store().save_changes(&changes).await.unwrap();
1856
1857        // `get_member` surfaces the status from the global profile.
1858        let member = room.get_member(user_id).await.expect("ok").expect("exists");
1859        let status = member.status().expect("status is set");
1860        assert_eq!(status.text, "Working");
1861        assert_eq!(status.emoji, "💻");
1862
1863        // `members` surfaces it too.
1864        let members = room.members(RoomMemberships::JOIN).await.unwrap();
1865        let member =
1866            members.iter().find(|m| m.user_id() == user_id).expect("member is in the list");
1867        let status = member.status().expect("status is set");
1868        assert_eq!(status.text, "Working");
1869        assert_eq!(status.emoji, "💻");
1870    }
1871
1872    #[async_test]
1873    async fn test_ignored_user_list_changes() {
1874        let user_id = user_id!("@alice:example.org");
1875        let client = BaseClient::new(
1876            StoreConfig::new(CrossProcessLockConfig::SingleProcess),
1877            ThreadingSupport::Disabled,
1878            DmRoomDefinition::default(),
1879        );
1880
1881        client
1882            .activate(
1883                SessionMeta { user_id: user_id.to_owned(), device_id: "FOOBAR".into() },
1884                RoomLoadSettings::default(),
1885                #[cfg(feature = "e2e-encryption")]
1886                None,
1887            )
1888            .await
1889            .unwrap();
1890
1891        let mut subscriber = client.subscribe_to_ignore_user_list_changes();
1892        assert!(subscriber.next().now_or_never().is_none());
1893
1894        let f = EventFactory::new();
1895        let mut sync_builder = SyncResponseBuilder::new();
1896        let response = sync_builder
1897            .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1898            .build_sync_response();
1899        client.receive_sync_response(response).await.unwrap();
1900
1901        assert_let!(Some(ignored) = subscriber.next().await);
1902        assert_eq!(ignored, [BOB.to_string()]);
1903
1904        // Receive the same response.
1905        let response = sync_builder
1906            .add_global_account_data(f.ignored_user_list([(*BOB).into()]))
1907            .build_sync_response();
1908        client.receive_sync_response(response).await.unwrap();
1909
1910        // No changes in the ignored list.
1911        assert!(subscriber.next().now_or_never().is_none());
1912
1913        // Now remove Bob from the ignored list.
1914        let response =
1915            sync_builder.add_global_account_data(f.ignored_user_list([])).build_sync_response();
1916        client.receive_sync_response(response).await.unwrap();
1917
1918        assert_let!(Some(ignored) = subscriber.next().await);
1919        assert!(ignored.is_empty());
1920    }
1921
1922    #[async_test]
1923    async fn test_is_user_ignored() {
1924        let ignored_user_id = user_id!("@alice:example.org");
1925        let client = logged_in_base_client(None).await;
1926
1927        let mut sync_builder = SyncResponseBuilder::new();
1928        let f = EventFactory::new();
1929        let response = sync_builder
1930            .add_global_account_data(f.ignored_user_list([ignored_user_id.to_owned()]))
1931            .build_sync_response();
1932        client.receive_sync_response(response).await.unwrap();
1933
1934        assert!(client.is_user_ignored(ignored_user_id).await);
1935    }
1936
1937    #[cfg(feature = "e2e-encryption")]
1938    #[async_test]
1939    async fn test_invite_details_are_set() {
1940        let user_id = user_id!("@alice:localhost");
1941        let client = logged_in_base_client(Some(user_id)).await;
1942        let known_room_id = room_id!("!invited:localhost");
1943        let unknown_room_id = room_id!("!unknown:localhost");
1944
1945        let mut sync_builder = SyncResponseBuilder::new();
1946        let response = sync_builder
1947            .add_invited_room(InvitedRoomBuilder::new(known_room_id))
1948            .build_sync_response();
1949        client.receive_sync_response(response).await.unwrap();
1950
1951        // Let us first check the initial state, we should have a room in the invite
1952        // state.
1953        let invited_room = client
1954            .get_room(known_room_id)
1955            .expect("The sync should have created a room in the invited state");
1956
1957        assert_eq!(invited_room.state(), RoomState::Invited);
1958        assert!(
1959            client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
1960        );
1961
1962        // Now we join the room.
1963        let joined_room = client
1964            .room_joined(known_room_id, Some(user_id.to_owned()))
1965            .await
1966            .expect("We should be able to mark a room as joined");
1967
1968        // Yup, we now have some invite details.
1969        assert_eq!(joined_room.state(), RoomState::Joined);
1970        assert_matches!(
1971            client.get_pending_key_bundle_details_for_room(known_room_id).await,
1972            Ok(Some(details))
1973        );
1974        assert_eq!(details.inviter, user_id);
1975
1976        // If we didn't know about the room before the join, we assume that there wasn't
1977        // an invite and we don't record the timestamp.
1978        assert!(client.get_room(unknown_room_id).is_none());
1979        let unknown_room = client
1980            .room_joined(unknown_room_id, Some(user_id.to_owned()))
1981            .await
1982            .expect("We should be able to mark a room as joined");
1983
1984        assert_eq!(unknown_room.state(), RoomState::Joined);
1985        assert!(
1986            client
1987                .get_pending_key_bundle_details_for_room(unknown_room_id)
1988                .await
1989                .unwrap()
1990                .is_none()
1991        );
1992
1993        sync_builder.clear();
1994        let response =
1995            sync_builder.add_left_room(LeftRoomBuilder::new(known_room_id)).build_sync_response();
1996        client.receive_sync_response(response).await.unwrap();
1997
1998        // Now that we left the room, we shouldn't have any details anymore.
1999        let left_room = client
2000            .get_room(known_room_id)
2001            .expect("The sync should have created a room in the invited state");
2002
2003        assert_eq!(left_room.state(), RoomState::Left);
2004        assert!(
2005            client.get_pending_key_bundle_details_for_room(known_room_id).await.unwrap().is_none()
2006        );
2007    }
2008}