Skip to main content

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, BTreeSet, HashSet},
31    sync::Arc,
32};
33
34pub use call::CallIntentConsensus;
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};
41pub use members::{RoomMember, RoomMembersUpdate, RoomMemberships};
42pub(crate) use room_info::SyncInfo;
43pub use room_info::{
44    BaseRoomInfo, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
45    apply_redaction,
46};
47use ruma::{
48    EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
49    RoomVersionId, UserId,
50    events::{
51        direct::OwnedDirectUserIdentifier,
52        receipt::{Receipt, ReceiptThread, ReceiptType},
53        room::{
54            avatar,
55            guest_access::GuestAccess,
56            history_visibility::HistoryVisibility,
57            join_rules::JoinRule,
58            member::MembershipState,
59            power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
60        },
61    },
62    room::RoomType,
63};
64use serde::{Deserialize, Serialize};
65pub use state::{RoomState, RoomStateFilter};
66pub(crate) use tags::RoomNotableTags;
67use tokio::sync::broadcast;
68pub use tombstone::{PredecessorRoom, SuccessorRoom};
69use tracing::{info, instrument, warn};
70
71use crate::{
72    Error,
73    deserialized_responses::MemberEvent,
74    notification_settings::RoomNotificationMode,
75    read_receipts::RoomReadReceipts,
76    store::{DynStateStore, Result as StoreResult, StateStoreExt},
77    sync::UnreadNotificationsCount,
78};
79
80/// The underlying room data structure collecting state for joined, left and
81/// invited rooms.
82#[derive(Debug, Clone)]
83pub struct Room {
84    /// The room ID.
85    pub(super) room_id: OwnedRoomId,
86
87    /// Our own user ID.
88    pub(super) own_user_id: OwnedUserId,
89
90    pub(super) info: SharedObservable<RoomInfo>,
91
92    /// A clone of the [`BaseStateStore::room_info_notable_update_sender`].
93    ///
94    /// [`BaseStateStore::room_info_notable_update_sender`]: crate::store::BaseStateStore::room_info_notable_update_sender
95    pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
96
97    /// A clone of the state store.
98    pub(super) store: Arc<DynStateStore>,
99
100    /// A map for ids of room membership events in the knocking state linked to
101    /// the user id of the user affected by the member event, that the current
102    /// user has marked as seen so they can be ignored.
103    pub seen_knock_request_ids_map:
104        SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
105
106    /// A sender that will notify receivers when room member updates happen.
107    pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
108}
109
110impl Room {
111    pub(crate) fn new(
112        own_user_id: &UserId,
113        store: Arc<DynStateStore>,
114        room_id: &RoomId,
115        room_state: RoomState,
116        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
117    ) -> Self {
118        let room_info = RoomInfo::new(room_id, room_state);
119        Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
120    }
121
122    pub(crate) fn restore(
123        own_user_id: &UserId,
124        store: Arc<DynStateStore>,
125        room_info: RoomInfo,
126        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
127    ) -> Self {
128        let (room_member_updates_sender, _) = broadcast::channel(10);
129        Self {
130            own_user_id: own_user_id.into(),
131            room_id: room_info.room_id.clone(),
132            store,
133            info: SharedObservable::new(room_info),
134            room_info_notable_update_sender,
135            seen_knock_request_ids_map: SharedObservable::new_async(None),
136            room_member_updates_sender,
137        }
138    }
139
140    /// Get the unique room id of the room.
141    pub fn room_id(&self) -> &RoomId {
142        &self.room_id
143    }
144
145    /// Get a copy of the room creators.
146    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
147        self.info.read().creators()
148    }
149
150    /// Get our own user id.
151    pub fn own_user_id(&self) -> &UserId {
152        &self.own_user_id
153    }
154
155    /// Whether this room's [`RoomType`] is `m.space`.
156    pub fn is_space(&self) -> bool {
157        self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
158    }
159
160    /// Whether this room is a Call room as defined by [MSC3417].
161    ///
162    /// [MSC3417]: <https://github.com/matrix-org/matrix-spec-proposals/pull/3417>
163    pub fn is_call(&self) -> bool {
164        self.info.read().room_type().is_some_and(|t| *t == RoomType::Call)
165    }
166
167    /// Returns the room's type as defined in its creation event
168    /// (`m.room.create`).
169    pub fn room_type(&self) -> Option<RoomType> {
170        self.info.read().room_type().map(ToOwned::to_owned)
171    }
172
173    /// Get the unread notification counts computed server-side.
174    ///
175    /// Note: these might be incorrect for encrypted rooms, since the server
176    /// doesn't know which events are relevant standalone messages or not,
177    /// nor can it inspect mentions. If you need more precise counts for
178    /// encrypted rooms, consider using the client-side computed counts in
179    /// [`Self::num_unread_messages`], [`Self::num_unread_notifications`] and
180    /// [`Self::num_unread_mentions`].
181    pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
182        self.info.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.info.read().read_receipts.num_unread
191    }
192
193    /// Get the number of unread notifications (computed client-side).
194    ///
195    /// This might be more precise than [`Self::unread_notification_counts`] for
196    /// encrypted rooms.
197    pub fn num_unread_notifications(&self) -> u64 {
198        self.info.read().read_receipts.num_notifications
199    }
200
201    /// Get the number of unread mentions (computed client-side), that is,
202    /// messages causing a highlight in a room.
203    ///
204    /// This might be more precise than [`Self::unread_notification_counts`] for
205    /// encrypted rooms.
206    pub fn num_unread_mentions(&self) -> u64 {
207        self.info.read().read_receipts.num_mentions
208    }
209
210    /// Get the detailed information about read receipts for the room.
211    pub fn read_receipts(&self) -> RoomReadReceipts {
212        self.info.read().read_receipts.clone()
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.info.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.info.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.info.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.info.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.info.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.info.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.info.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        Some(self.info.read().base_info.create.as_ref()?.content.clone())
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.info.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.info.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.info.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.info.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.info.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.info.read().history_visibility_or_default().clone()
339    }
340
341    /// Is the room considered to be public.
342    ///
343    /// May return `None` if the join rule event is not available.
344    pub fn is_public(&self) -> Option<bool> {
345        self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
346    }
347
348    /// Get the join rule policy of this room, if available.
349    pub fn join_rule(&self) -> Option<JoinRule> {
350        self.info.read().join_rule().cloned()
351    }
352
353    /// Get the maximum power level that this room contains.
354    ///
355    /// This is useful if one wishes to normalize the power levels, e.g. from
356    /// 0-100 where 100 would be the max power level.
357    pub fn max_power_level(&self) -> i64 {
358        self.info.read().base_info.max_power_level
359    }
360
361    /// Get the service members in this room, if available.
362    pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
363        self.info.read().service_members().cloned()
364    }
365
366    /// Get the current power levels of this room.
367    pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
368        let power_levels_content = self
369            .store
370            .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
371            .await?
372            .ok_or(Error::InsufficientData)?
373            .deserialize()?;
374        let creators = self.creators().ok_or(Error::InsufficientData)?;
375        let rules = self.info.read().room_version_rules_or_default();
376
377        Ok(power_levels_content.power_levels(&rules.authorization, creators))
378    }
379
380    /// Get the current power levels of this room, or a sensible default if they
381    /// are not known.
382    pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
383        if let Ok(power_levels) = self.power_levels().await {
384            return power_levels;
385        }
386
387        // As a fallback, create the default power levels of a room.
388        let rules = self.info.read().room_version_rules_or_default();
389        RoomPowerLevels::new(
390            RoomPowerLevelsSource::None,
391            &rules.authorization,
392            self.creators().into_iter().flatten(),
393        )
394    }
395
396    /// Get the `m.room.name` of this room.
397    ///
398    /// The returned string may be empty if the event has been redacted, or it's
399    /// missing from storage.
400    pub fn name(&self) -> Option<String> {
401        self.info.read().name().map(ToOwned::to_owned)
402    }
403
404    /// Get the topic of the room.
405    pub fn topic(&self) -> Option<String> {
406        self.info.read().topic().map(ToOwned::to_owned)
407    }
408
409    /// Update the cached user defined notification mode.
410    ///
411    /// This is automatically recomputed on every successful sync, and the
412    /// cached result can be retrieved in
413    /// [`Self::cached_user_defined_notification_mode`].
414    pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
415        self.info.update_if(|info| {
416            if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
417                info.cached_user_defined_notification_mode = Some(mode);
418
419                true
420            } else {
421                false
422            }
423        });
424    }
425
426    /// Returns the cached user defined notification mode, if available.
427    ///
428    /// This cache is refilled every time we call
429    /// [`Self::update_cached_user_defined_notification_mode`].
430    pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
431        self.info.read().cached_user_defined_notification_mode
432    }
433
434    /// Removes any existing cached value for the user defined notification
435    /// mode.
436    pub fn clear_user_defined_notification_mode(&self) {
437        self.info.update_if(|info| {
438            if info.cached_user_defined_notification_mode.is_some() {
439                info.cached_user_defined_notification_mode = None;
440                true
441            } else {
442                false
443            }
444        })
445    }
446
447    /// Get the list of users ids that are considered to be joined members of
448    /// this room.
449    pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
450        self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
451    }
452
453    /// Get the heroes for this room.
454    pub fn heroes(&self) -> Vec<RoomHero> {
455        self.info.read().heroes().to_vec()
456    }
457
458    /// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
459    /// `receipt_type`, `thread` and `user_id` in this room.
460    pub async fn load_user_receipt(
461        &self,
462        receipt_type: ReceiptType,
463        thread: ReceiptThread,
464        user_id: &UserId,
465    ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
466        self.store.get_user_room_receipt_event(self.room_id(), receipt_type, thread, user_id).await
467    }
468
469    /// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
470    /// tuples for the given `receipt_type`, `thread` and `event_id` in this
471    /// room.
472    pub async fn load_event_receipts(
473        &self,
474        receipt_type: ReceiptType,
475        thread: ReceiptThread,
476        event_id: &EventId,
477    ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
478        self.store
479            .get_event_room_receipt_events(self.room_id(), receipt_type, thread, event_id)
480            .await
481    }
482
483    /// Returns a boolean indicating if this room has been manually marked as
484    /// unread
485    pub fn is_marked_unread(&self) -> bool {
486        self.info.read().base_info.is_marked_unread
487    }
488
489    /// Returns the [`RoomVersionId`] of the room, if known.
490    pub fn version(&self) -> Option<RoomVersionId> {
491        self.info.read().room_version().cloned()
492    }
493
494    /// Returns the recency stamp of the room.
495    ///
496    /// Please read `RoomInfo::recency_stamp` to learn more.
497    pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
498        self.info.read().recency_stamp
499    }
500
501    /// Get a `Stream` of loaded pinned events for this room.
502    /// If no pinned events are found a single empty `Vec` will be returned.
503    pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
504        self.info
505            .subscribe()
506            .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
507    }
508
509    /// Returns the current pinned event ids for this room.
510    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
511        self.info.read().pinned_event_ids()
512    }
513
514    /// Returns the list of service members that are either in a joined or
515    /// invited state in this room, checking the service member list against the
516    /// locally available room members.
517    pub async fn active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
518        if let Some(service_members) = self.service_members() {
519            let mut found = Vec::new();
520            for user_id in service_members {
521                match self.get_member(&user_id).await {
522                    Ok(Some(member)) => {
523                        // We only care about active members (joined or invited)
524                        if matches!(
525                            member.membership(),
526                            MembershipState::Join | MembershipState::Invite
527                        ) {
528                            found.push(member);
529                        }
530                    }
531                    Ok(None) => (),
532                    Err(error) => return Err(error),
533                }
534            }
535
536            Ok(Some(found))
537        } else {
538            Ok(None)
539        }
540    }
541}
542
543// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
544#[cfg(not(feature = "test-send-sync"))]
545unsafe impl Send for Room {}
546
547// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
548#[cfg(not(feature = "test-send-sync"))]
549unsafe impl Sync for Room {}
550
551#[cfg(feature = "test-send-sync")]
552#[test]
553// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
554fn test_send_sync_for_room() {
555    fn assert_send_sync<
556        T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
557    >() {
558    }
559
560    assert_send_sync::<Room>();
561}
562
563/// The possible sources of an account data type.
564#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
565pub(crate) enum AccountDataSource {
566    /// The source is account data with the stable prefix.
567    Stable,
568
569    /// The source is account data with the unstable prefix.
570    #[default]
571    Unstable,
572}