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