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