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