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