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