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