Skip to main content

matrix_sdk_base/
client.rs

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