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