Skip to main content

matrix_sdk_base/
client.rs

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