matrix_sdk_base/room/
mod.rs

1// Copyright 2025 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![allow(clippy::assign_op_pattern)] // Triggered by bitflags! usage
16
17mod call;
18mod create;
19mod display_name;
20mod encryption;
21mod knock;
22mod latest_event;
23mod members;
24mod room_info;
25mod state;
26mod tags;
27mod tombstone;
28
29#[cfg(feature = "e2e-encryption")]
30use std::sync::RwLock as SyncRwLock;
31use std::{
32    collections::{BTreeMap, HashSet},
33    sync::Arc,
34};
35
36pub use create::*;
37pub use display_name::{RoomDisplayName, RoomHero};
38pub(crate) use display_name::{RoomSummary, UpdatedRoomDisplayName};
39pub use encryption::EncryptionState;
40use eyeball::{AsyncLock, SharedObservable};
41use futures_util::{Stream, StreamExt};
42#[cfg(feature = "e2e-encryption")]
43use matrix_sdk_common::ring_buffer::RingBuffer;
44pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
45pub(crate) use room_info::SyncInfo;
46pub use room_info::{
47    BaseRoomInfo, InviteAcceptanceDetails, RoomInfo, RoomInfoNotableUpdate,
48    RoomInfoNotableUpdateReasons, apply_redaction,
49};
50use ruma::{
51    EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
52    RoomVersionId, UserId,
53    events::{
54        direct::OwnedDirectUserIdentifier,
55        receipt::{Receipt, ReceiptThread, ReceiptType},
56        room::{
57            avatar,
58            guest_access::GuestAccess,
59            history_visibility::HistoryVisibility,
60            join_rules::JoinRule,
61            power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
62        },
63    },
64    room::RoomType,
65};
66#[cfg(feature = "e2e-encryption")]
67use ruma::{events::AnySyncTimelineEvent, serde::Raw};
68use serde::{Deserialize, Serialize};
69pub use state::{RoomState, RoomStateFilter};
70pub(crate) use tags::RoomNotableTags;
71use tokio::sync::broadcast;
72pub use tombstone::{PredecessorRoom, SuccessorRoom};
73use tracing::{info, instrument, warn};
74
75use crate::{
76    Error, MinimalStateEvent,
77    deserialized_responses::MemberEvent,
78    notification_settings::RoomNotificationMode,
79    read_receipts::RoomReadReceipts,
80    store::{DynStateStore, Result as StoreResult, StateStoreExt},
81    sync::UnreadNotificationsCount,
82};
83
84/// The underlying room data structure collecting state for joined, left and
85/// invited rooms.
86#[derive(Debug, Clone)]
87pub struct Room {
88    /// The room ID.
89    pub(super) room_id: OwnedRoomId,
90
91    /// Our own user ID.
92    pub(super) own_user_id: OwnedUserId,
93
94    pub(super) inner: SharedObservable<RoomInfo>,
95    pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
96    pub(super) store: Arc<DynStateStore>,
97
98    /// The most recent few encrypted events. When the keys come through to
99    /// decrypt these, the most recent relevant one will replace
100    /// `latest_event`. (We can't tell which one is relevant until
101    /// they are decrypted.)
102    ///
103    /// Currently, these are held in Room rather than RoomInfo, because we were
104    /// not sure whether holding too many of them might make the cache too
105    /// slow to load on startup. Keeping them here means they are not cached
106    /// to disk but held in memory.
107    #[cfg(feature = "e2e-encryption")]
108    pub latest_encrypted_events: Arc<SyncRwLock<RingBuffer<Raw<AnySyncTimelineEvent>>>>,
109
110    /// A map for ids of room membership events in the knocking state linked to
111    /// the user id of the user affected by the member event, that the current
112    /// user has marked as seen so they can be ignored.
113    pub seen_knock_request_ids_map:
114        SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
115
116    /// A sender that will notify receivers when room member updates happen.
117    pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
118}
119
120impl Room {
121    pub(crate) fn new(
122        own_user_id: &UserId,
123        store: Arc<DynStateStore>,
124        room_id: &RoomId,
125        room_state: RoomState,
126        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
127    ) -> Self {
128        let room_info = RoomInfo::new(room_id, room_state);
129        Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
130    }
131
132    pub(crate) fn restore(
133        own_user_id: &UserId,
134        store: Arc<DynStateStore>,
135        room_info: RoomInfo,
136        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
137    ) -> Self {
138        let (room_member_updates_sender, _) = broadcast::channel(10);
139        Self {
140            own_user_id: own_user_id.into(),
141            room_id: room_info.room_id.clone(),
142            store,
143            inner: SharedObservable::new(room_info),
144            #[cfg(feature = "e2e-encryption")]
145            latest_encrypted_events: Arc::new(SyncRwLock::new(RingBuffer::new(
146                Self::MAX_ENCRYPTED_EVENTS,
147            ))),
148            room_info_notable_update_sender,
149            seen_knock_request_ids_map: SharedObservable::new_async(None),
150            room_member_updates_sender,
151        }
152    }
153
154    /// Get the unique room id of the room.
155    pub fn room_id(&self) -> &RoomId {
156        &self.room_id
157    }
158
159    /// Get a copy of the room creators.
160    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
161        self.inner.read().creators()
162    }
163
164    /// Get our own user id.
165    pub fn own_user_id(&self) -> &UserId {
166        &self.own_user_id
167    }
168
169    /// Whether this room's [`RoomType`] is `m.space`.
170    pub fn is_space(&self) -> bool {
171        self.inner.read().room_type().is_some_and(|t| *t == RoomType::Space)
172    }
173
174    /// Returns the room's type as defined in its creation event
175    /// (`m.room.create`).
176    pub fn room_type(&self) -> Option<RoomType> {
177        self.inner.read().room_type().map(ToOwned::to_owned)
178    }
179
180    /// Get the unread notification counts.
181    pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
182        self.inner.read().notification_counts
183    }
184
185    /// Get the number of unread messages (computed client-side).
186    ///
187    /// This might be more precise than [`Self::unread_notification_counts`] for
188    /// encrypted rooms.
189    pub fn num_unread_messages(&self) -> u64 {
190        self.inner.read().read_receipts.num_unread
191    }
192
193    /// Get the detailed information about read receipts for the room.
194    pub fn read_receipts(&self) -> RoomReadReceipts {
195        self.inner.read().read_receipts.clone()
196    }
197
198    /// Get the number of unread notifications (computed client-side).
199    ///
200    /// This might be more precise than [`Self::unread_notification_counts`] for
201    /// encrypted rooms.
202    pub fn num_unread_notifications(&self) -> u64 {
203        self.inner.read().read_receipts.num_notifications
204    }
205
206    /// Get the number of unread mentions (computed client-side), that is,
207    /// messages causing a highlight in a room.
208    ///
209    /// This might be more precise than [`Self::unread_notification_counts`] for
210    /// encrypted rooms.
211    pub fn num_unread_mentions(&self) -> u64 {
212        self.inner.read().read_receipts.num_mentions
213    }
214
215    /// Check if the room states have been synced
216    ///
217    /// States might be missing if we have only seen the room_id of this Room
218    /// so far, for example as the response for a `create_room` request without
219    /// being synced yet.
220    ///
221    /// Returns true if the state is fully synced, false otherwise.
222    pub fn is_state_fully_synced(&self) -> bool {
223        self.inner.read().sync_info == SyncInfo::FullySynced
224    }
225
226    /// Check if the room state has been at least partially synced.
227    ///
228    /// See [`Room::is_state_fully_synced`] for more info.
229    pub fn is_state_partially_or_fully_synced(&self) -> bool {
230        self.inner.read().sync_info != SyncInfo::NoState
231    }
232
233    /// Get the `prev_batch` token that was received from the last sync. May be
234    /// `None` if the last sync contained the full room history.
235    pub fn last_prev_batch(&self) -> Option<String> {
236        self.inner.read().last_prev_batch.clone()
237    }
238
239    /// Get the avatar url of this room.
240    pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
241        self.inner.read().avatar_url().map(ToOwned::to_owned)
242    }
243
244    /// Get information about the avatar of this room.
245    pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
246        self.inner.read().avatar_info().map(ToOwned::to_owned)
247    }
248
249    /// Get the canonical alias of this room.
250    pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
251        self.inner.read().canonical_alias().map(ToOwned::to_owned)
252    }
253
254    /// Get the canonical alias of this room.
255    pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
256        self.inner.read().alt_aliases().to_owned()
257    }
258
259    /// Get the `m.room.create` content of this room.
260    ///
261    /// This usually isn't optional but some servers might not send an
262    /// `m.room.create` event as the first event for a given room, thus this can
263    /// be optional.
264    ///
265    /// For room versions earlier than room version 11, if the event is
266    /// redacted, all fields except `creator` will be set to their default
267    /// value.
268    pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
269        match self.inner.read().base_info.create.as_ref()? {
270            MinimalStateEvent::Original(ev) => Some(ev.content.clone()),
271            MinimalStateEvent::Redacted(ev) => Some(ev.content.clone()),
272        }
273    }
274
275    /// Is this room considered a direct message.
276    ///
277    /// Async because it can read room info from storage.
278    #[instrument(skip_all, fields(room_id = ?self.room_id))]
279    pub async fn is_direct(&self) -> StoreResult<bool> {
280        match self.state() {
281            RoomState::Joined | RoomState::Left | RoomState::Banned => {
282                Ok(!self.inner.read().base_info.dm_targets.is_empty())
283            }
284
285            RoomState::Invited => {
286                let member = self.get_member(self.own_user_id()).await?;
287
288                match member {
289                    None => {
290                        info!("RoomMember not found for the user's own id");
291                        Ok(false)
292                    }
293                    Some(member) => match member.event.as_ref() {
294                        MemberEvent::Sync(_) => {
295                            warn!("Got MemberEvent::Sync in an invited room");
296                            Ok(false)
297                        }
298                        MemberEvent::Stripped(event) => {
299                            Ok(event.content.is_direct.unwrap_or(false))
300                        }
301                    },
302                }
303            }
304
305            // TODO: implement logic once we have the stripped events as we'd have with an Invite
306            RoomState::Knocked => Ok(false),
307        }
308    }
309
310    /// If this room is a direct message, get the members that we're sharing the
311    /// room with.
312    ///
313    /// *Note*: The member list might have been modified in the meantime and
314    /// the targets might not even be in the room anymore. This setting should
315    /// only be considered as guidance. We leave members in this list to allow
316    /// us to re-find a DM with a user even if they have left, since we may
317    /// want to re-invite them.
318    pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
319        self.inner.read().base_info.dm_targets.clone()
320    }
321
322    /// If this room is a direct message, returns the number of members that
323    /// we're sharing the room with.
324    pub fn direct_targets_length(&self) -> usize {
325        self.inner.read().base_info.dm_targets.len()
326    }
327
328    /// Get the guest access policy of this room.
329    pub fn guest_access(&self) -> GuestAccess {
330        self.inner.read().guest_access().clone()
331    }
332
333    /// Get the history visibility policy of this room.
334    pub fn history_visibility(&self) -> Option<HistoryVisibility> {
335        self.inner.read().history_visibility().cloned()
336    }
337
338    /// Get the history visibility policy of this room, or a sensible default if
339    /// the event is missing.
340    pub fn history_visibility_or_default(&self) -> HistoryVisibility {
341        self.inner.read().history_visibility_or_default().clone()
342    }
343
344    /// Is the room considered to be public.
345    ///
346    /// May return `None` if the join rule event is not available.
347    pub fn is_public(&self) -> Option<bool> {
348        self.inner.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
349    }
350
351    /// Get the join rule policy of this room, if available.
352    pub fn join_rule(&self) -> Option<JoinRule> {
353        self.inner.read().join_rule().cloned()
354    }
355
356    /// Get the maximum power level that this room contains.
357    ///
358    /// This is useful if one wishes to normalize the power levels, e.g. from
359    /// 0-100 where 100 would be the max power level.
360    pub fn max_power_level(&self) -> i64 {
361        self.inner.read().base_info.max_power_level
362    }
363
364    /// Get the current power levels of this room.
365    pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
366        let power_levels_content = self
367            .store
368            .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
369            .await?
370            .ok_or(Error::InsufficientData)?
371            .deserialize()?;
372        let creators = self.creators().ok_or(Error::InsufficientData)?;
373        let rules = self.inner.read().room_version_rules_or_default();
374
375        Ok(power_levels_content.power_levels(&rules.authorization, creators))
376    }
377
378    /// Get the current power levels of this room, or a sensible default if they
379    /// are not known.
380    pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
381        if let Ok(power_levels) = self.power_levels().await {
382            return power_levels;
383        }
384
385        // As a fallback, create the default power levels of a room.
386        let rules = self.inner.read().room_version_rules_or_default();
387        RoomPowerLevels::new(
388            RoomPowerLevelsSource::None,
389            &rules.authorization,
390            self.creators().into_iter().flatten(),
391        )
392    }
393
394    /// Get the `m.room.name` of this room.
395    ///
396    /// The returned string may be empty if the event has been redacted, or it's
397    /// missing from storage.
398    pub fn name(&self) -> Option<String> {
399        self.inner.read().name().map(ToOwned::to_owned)
400    }
401
402    /// Get the topic of the room.
403    pub fn topic(&self) -> Option<String> {
404        self.inner.read().topic().map(ToOwned::to_owned)
405    }
406
407    /// Update the cached user defined notification mode.
408    ///
409    /// This is automatically recomputed on every successful sync, and the
410    /// cached result can be retrieved in
411    /// [`Self::cached_user_defined_notification_mode`].
412    pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
413        self.inner.update_if(|info| {
414            if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
415                info.cached_user_defined_notification_mode = Some(mode);
416
417                true
418            } else {
419                false
420            }
421        });
422    }
423
424    /// Returns the cached user defined notification mode, if available.
425    ///
426    /// This cache is refilled every time we call
427    /// [`Self::update_cached_user_defined_notification_mode`].
428    pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
429        self.inner.read().cached_user_defined_notification_mode
430    }
431
432    /// Get the list of users ids that are considered to be joined members of
433    /// this room.
434    pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
435        self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
436    }
437
438    /// Get the heroes for this room.
439    pub fn heroes(&self) -> Vec<RoomHero> {
440        self.inner.read().heroes().to_vec()
441    }
442
443    /// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
444    /// `receipt_type`, `thread` and `user_id` in this room.
445    pub async fn load_user_receipt(
446        &self,
447        receipt_type: ReceiptType,
448        thread: ReceiptThread,
449        user_id: &UserId,
450    ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
451        self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
452    }
453
454    /// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
455    /// tuples for the given `receipt_type`, `thread` and `event_id` in this
456    /// room.
457    pub async fn load_event_receipts(
458        &self,
459        receipt_type: ReceiptType,
460        thread: ReceiptThread,
461        event_id: &EventId,
462    ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
463        self.store
464            .get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
465            .await
466    }
467
468    /// Returns a boolean indicating if this room has been manually marked as
469    /// unread
470    pub fn is_marked_unread(&self) -> bool {
471        self.inner.read().base_info.is_marked_unread
472    }
473
474    /// Returns the [`RoomVersionId`] of the room, if known.
475    pub fn version(&self) -> Option<RoomVersionId> {
476        self.inner.read().room_version().cloned()
477    }
478
479    /// Returns the recency stamp of the room.
480    ///
481    /// Please read `RoomInfo::recency_stamp` to learn more.
482    pub fn recency_stamp(&self) -> Option<u64> {
483        self.inner.read().recency_stamp
484    }
485
486    /// Returns the details about an invite to this room if the invite has been
487    /// accepted by this specific client.
488    ///
489    /// # Returns
490    /// - `Some` if an invite has been accepted by this specific client.
491    /// - `None` if we didn't join this room using an invite or the invite
492    ///   wasn't accepted by this client.
493    pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
494        self.inner.read().invite_acceptance_details.clone()
495    }
496
497    /// Get a `Stream` of loaded pinned events for this room.
498    /// If no pinned events are found a single empty `Vec` will be returned.
499    pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
500        self.inner
501            .subscribe()
502            .map(|i| i.base_info.pinned_events.map(|c| c.pinned).unwrap_or_default())
503    }
504
505    /// Returns the current pinned event ids for this room.
506    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
507        self.inner.read().pinned_event_ids()
508    }
509}
510
511// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
512#[cfg(not(feature = "test-send-sync"))]
513unsafe impl Send for Room {}
514
515// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
516#[cfg(not(feature = "test-send-sync"))]
517unsafe impl Sync for Room {}
518
519#[cfg(feature = "test-send-sync")]
520#[test]
521// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
522fn test_send_sync_for_room() {
523    fn assert_send_sync<
524        T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
525    >() {
526    }
527
528    assert_send_sync::<Room>();
529}
530
531/// The possible sources of an account data type.
532#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
533pub(crate) enum AccountDataSource {
534    /// The source is account data with the stable prefix.
535    Stable,
536
537    /// The source is account data with the unstable prefix.
538    #[default]
539    Unstable,
540}