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::collections::{BTreeMap, BTreeSet, HashSet};
30#[cfg(feature = "unstable-msc4354")]
31use std::sync::{Arc, OnceLock};
32
33pub use call::CallIntentConsensus;
34pub use create::*;
35pub use display_name::{RoomDisplayName, RoomHero, RoomHeroWithProfile};
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, RoomInfo, RoomInfoNotableUpdate, RoomInfoNotableUpdateReasons, RoomRecencyStamp,
44    apply_redaction,
45};
46#[cfg(feature = "unstable-msc4426")]
47use ruma::profile::{Call, Status};
48use ruma::{
49    EventId, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId, RoomId,
50    RoomVersionId, UserId,
51    events::{
52        direct::OwnedDirectUserIdentifier,
53        receipt::{Receipt, ReceiptThread, ReceiptType},
54        room::{
55            avatar,
56            guest_access::GuestAccess,
57            history_visibility::HistoryVisibility,
58            join_rules::JoinRule,
59            member::MembershipState,
60            power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent, RoomPowerLevelsSource},
61            retention::RoomRetentionEventContent,
62        },
63    },
64    room::RoomType,
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, trace, warn};
72
73use crate::{
74    DmRoomDefinition, Error, StateStore,
75    deserialized_responses::MemberEvent,
76    notification_settings::RoomNotificationMode,
77    read_receipts::ReadReceipts,
78    store::{Result as StoreResult, SaveLockedStateStore, StateStoreExt},
79    sync::UnreadNotificationsCount,
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) info: SharedObservable<RoomInfo>,
93
94    /// A clone of the [`BaseStateStore::room_info_notable_update_sender`].
95    ///
96    /// [`BaseStateStore::room_info_notable_update_sender`]: crate::store::BaseStateStore::room_info_notable_update_sender
97    pub(super) room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
98
99    /// A clone of the state store.
100    pub(super) store: SaveLockedStateStore,
101
102    /// A map for ids of room membership events in the knocking state linked to
103    /// the user id of the user affected by the member event, that the current
104    /// user has marked as seen so they can be ignored.
105    pub seen_knock_request_ids_map:
106        SharedObservable<Option<BTreeMap<OwnedEventId, OwnedUserId>>, AsyncLock>,
107
108    /// A sender that will notify receivers when room member updates happen.
109    pub room_member_updates_sender: broadcast::Sender<RoomMembersUpdate>,
110
111    /// The sticky events (MSC4354) of this room.
112    ///
113    /// Created on first use, so that rooms that never see a sticky
114    /// event don't pay for a map and its maintenance task.
115    #[cfg(feature = "unstable-msc4354")]
116    pub(super) sticky_events: Arc<OnceLock<crate::sticky::StickyEvents>>,
117}
118
119impl Room {
120    pub(crate) fn new(
121        own_user_id: &UserId,
122        store: SaveLockedStateStore,
123        room_id: &RoomId,
124        room_state: RoomState,
125        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
126    ) -> Self {
127        let room_info = RoomInfo::new(room_id, room_state);
128        Self::restore(own_user_id, store, room_info, room_info_notable_update_sender)
129    }
130
131    pub(crate) fn restore(
132        own_user_id: &UserId,
133        store: SaveLockedStateStore,
134        room_info: RoomInfo,
135        room_info_notable_update_sender: broadcast::Sender<RoomInfoNotableUpdate>,
136    ) -> Self {
137        let (room_member_updates_sender, _) = broadcast::channel(10);
138        Self {
139            own_user_id: own_user_id.into(),
140            room_id: room_info.room_id.clone(),
141            store,
142            info: SharedObservable::new(room_info),
143            room_info_notable_update_sender,
144            seen_knock_request_ids_map: SharedObservable::new_async(None),
145            room_member_updates_sender,
146            #[cfg(feature = "unstable-msc4354")]
147            sticky_events: Default::default(),
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    /// The sticky events ([MSC4354]) of this room.
157    ///
158    /// [MSC4354]: https://github.com/matrix-org/matrix-spec-proposals/pull/4354
159    #[cfg(feature = "unstable-msc4354")]
160    pub fn sticky_events(&self) -> &crate::sticky::StickyEvents {
161        self.sticky_events.get_or_init(|| crate::sticky::StickyEvents::new(self.room_id.clone()))
162    }
163
164    /// The sticky events of this room, if it ever had any.
165    ///
166    /// Unlike [`Room::sticky_events`], this doesn't create the map of a room
167    /// that has never seen a sticky event.
168    #[cfg(feature = "unstable-msc4354")]
169    pub(crate) fn sticky_events_if_any(&self) -> Option<&crate::sticky::StickyEvents> {
170        self.sticky_events.get()
171    }
172
173    /// Get a copy of the room creators.
174    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
175        self.info.read().creators()
176    }
177
178    /// Get our own user id.
179    pub fn own_user_id(&self) -> &UserId {
180        &self.own_user_id
181    }
182
183    /// Whether this room's [`RoomType`] is `m.space`.
184    pub fn is_space(&self) -> bool {
185        self.info.read().room_type().is_some_and(|t| *t == RoomType::Space)
186    }
187
188    /// Whether this room is a Call room as defined by [MSC3417].
189    ///
190    /// [MSC3417]: <https://github.com/matrix-org/matrix-spec-proposals/pull/3417>
191    pub fn is_call(&self) -> bool {
192        self.info.read().room_type().is_some_and(|t| *t == RoomType::Call)
193    }
194
195    /// Returns the room's type as defined in its creation event
196    /// (`m.room.create`).
197    pub fn room_type(&self) -> Option<RoomType> {
198        self.info.read().room_type().map(ToOwned::to_owned)
199    }
200
201    /// Get the unread notification counts computed server-side.
202    ///
203    /// Note: these might be incorrect for encrypted rooms, since the server
204    /// doesn't know which events are relevant standalone messages or not, nor
205    /// can it inspect mentions. If you need more precise counts for encrypted
206    /// rooms, consider using the client-side computed counts in
207    /// [`Self::num_unread_messages`], [`Self::num_unread_notifications`] and
208    /// [`Self::num_unread_mentions`].
209    pub fn unread_notification_counts(&self) -> UnreadNotificationsCount {
210        self.info.read().notification_counts
211    }
212
213    /// Get the number of unread messages (computed client-side).
214    ///
215    /// This might be more precise than [`Self::unread_notification_counts`] for
216    /// encrypted rooms.
217    pub fn num_unread_messages(&self) -> u64 {
218        self.info.read().read_receipts.num_unread
219    }
220
221    /// Get the number of unread notifications (computed client-side).
222    ///
223    /// This might be more precise than [`Self::unread_notification_counts`] for
224    /// encrypted rooms.
225    pub fn num_unread_notifications(&self) -> u64 {
226        self.info.read().read_receipts.num_notifications
227    }
228
229    /// Get the number of unread mentions (computed client-side), that is,
230    /// messages causing a highlight in a room.
231    ///
232    /// This might be more precise than [`Self::unread_notification_counts`] for
233    /// encrypted rooms.
234    pub fn num_unread_mentions(&self) -> u64 {
235        self.info.read().read_receipts.num_mentions
236    }
237
238    /// Get the detailed information about read receipts for the room.
239    pub fn read_receipts(&self) -> ReadReceipts {
240        self.info.read().read_receipts.clone()
241    }
242
243    /// Check if the room states have been synced
244    ///
245    /// States might be missing if we have only seen the room_id of this Room so
246    /// far, for example as the response for a `create_room` request without
247    /// being synced yet.
248    ///
249    /// Returns true if the state is fully synced, false otherwise.
250    pub fn is_state_fully_synced(&self) -> bool {
251        self.info.read().sync_info == SyncInfo::FullySynced
252    }
253
254    /// Check if the room state has been at least partially synced.
255    ///
256    /// See [`Room::is_state_fully_synced`] for more info.
257    pub fn is_state_partially_or_fully_synced(&self) -> bool {
258        self.info.read().sync_info != SyncInfo::NoState
259    }
260
261    /// Get the `prev_batch` token that was received from the last sync. May be
262    /// `None` if the last sync contained the full room history.
263    pub fn last_prev_batch(&self) -> Option<String> {
264        self.info.read().last_prev_batch.clone()
265    }
266
267    /// Get the avatar url of this room.
268    pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
269        self.info.read().avatar_url().map(ToOwned::to_owned)
270    }
271
272    /// Get information about the avatar of this room.
273    pub fn avatar_info(&self) -> Option<avatar::ImageInfo> {
274        self.info.read().avatar_info().map(ToOwned::to_owned)
275    }
276
277    /// Get the canonical alias of this room.
278    pub fn canonical_alias(&self) -> Option<OwnedRoomAliasId> {
279        self.info.read().canonical_alias().map(ToOwned::to_owned)
280    }
281
282    /// Get the canonical alias of this room.
283    pub fn alt_aliases(&self) -> Vec<OwnedRoomAliasId> {
284        self.info.read().alt_aliases().to_owned()
285    }
286
287    /// Get the `m.room.create` content of this room.
288    ///
289    /// This usually isn't optional but some servers might not send an
290    /// `m.room.create` event as the first event for a given room, thus this can
291    /// be optional.
292    ///
293    /// For room versions earlier than room version 11, if the event is
294    /// redacted, all fields except `creator` will be set to their default
295    /// value.
296    pub fn create_content(&self) -> Option<RoomCreateWithCreatorEventContent> {
297        Some(self.info.read().base_info.create.as_ref()?.content.clone())
298    }
299
300    /// Is this room considered a direct message.
301    ///
302    /// Async because it can read room info from storage.
303    #[instrument(skip_all, fields(room_id = ?self.room_id))]
304    pub async fn is_direct(&self) -> StoreResult<bool> {
305        match self.state() {
306            RoomState::Joined | RoomState::Left | RoomState::Banned => {
307                Ok(!self.info.read().base_info.dm_targets.is_empty())
308            }
309
310            RoomState::Invited => {
311                let member = self.get_member(self.own_user_id()).await?;
312
313                match member {
314                    None => {
315                        info!("RoomMember not found for the user's own id");
316                        Ok(false)
317                    }
318                    Some(member) => match member.event.as_ref() {
319                        MemberEvent::Sync(_) => {
320                            warn!("Got MemberEvent::Sync in an invited room");
321                            Ok(false)
322                        }
323                        MemberEvent::Stripped(event) => {
324                            Ok(event.content.is_direct.unwrap_or(false))
325                        }
326                    },
327                }
328            }
329
330            // TODO: implement logic once we have the stripped events as we'd have with an Invite
331            RoomState::Knocked => Ok(false),
332        }
333    }
334
335    /// Computes if the current room is a DM based on the rules from the
336    /// [`DmRoomDefinition`], updating the active service members.
337    pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
338        let is_direct = self.is_direct().await?;
339
340        match *dm_room_definition {
341            DmRoomDefinition::MatrixSpec => Ok(is_direct),
342            DmRoomDefinition::TwoMembers => {
343                if !is_direct {
344                    return Ok(false);
345                }
346                let active_service_member_count =
347                    self.update_active_service_members().await?.unwrap_or_default().len() as u64;
348                let has_at_most_two_members =
349                    self.active_members_count().saturating_sub(active_service_member_count) <= 2;
350                Ok(has_at_most_two_members)
351            }
352        }
353    }
354
355    /// If this room is a direct message, get the members that we're sharing the
356    /// room with.
357    ///
358    /// _Note_: The member list might have been modified in the meantime and the
359    /// targets might not even be in the room anymore. This setting should only
360    /// be considered as guidance. We leave members in this list to allow us to
361    /// re-find a DM with a user even if they have left, since we may want to
362    /// re-invite them.
363    pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
364        self.info.read().base_info.dm_targets.clone()
365    }
366
367    /// If this room is a direct message, returns the number of members that
368    /// we're sharing the room with.
369    pub fn direct_targets_length(&self) -> usize {
370        self.info.read().base_info.dm_targets.len()
371    }
372
373    /// Get the guest access policy of this room.
374    pub fn guest_access(&self) -> GuestAccess {
375        self.info.read().guest_access().clone()
376    }
377
378    /// Get the history visibility policy of this room.
379    pub fn history_visibility(&self) -> Option<HistoryVisibility> {
380        self.info.read().history_visibility().cloned()
381    }
382
383    /// Get the history visibility policy of this room, or a sensible default if
384    /// the event is missing.
385    pub fn history_visibility_or_default(&self) -> HistoryVisibility {
386        self.info.read().history_visibility_or_default().clone()
387    }
388
389    /// Is the room considered to be public.
390    ///
391    /// May return `None` if the join rule event is not available.
392    pub fn is_public(&self) -> Option<bool> {
393        self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
394    }
395
396    /// Get the join rule policy of this room, if available.
397    pub fn join_rule(&self) -> Option<JoinRule> {
398        self.info.read().join_rule().cloned()
399    }
400
401    /// Get the maximum power level that this room contains.
402    ///
403    /// This is useful if one wishes to normalize the power levels, e.g. from
404    /// 0-100 where 100 would be the max power level.
405    pub fn max_power_level(&self) -> i64 {
406        self.info.read().base_info.max_power_level
407    }
408
409    /// Get the message retention policy of this room, if set.
410    pub fn retention(&self) -> Option<RoomRetentionEventContent> {
411        self.info.read().retention().cloned()
412    }
413
414    /// Get the service members in this room, if available.
415    pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
416        self.info.read().service_members().cloned()
417    }
418
419    /// Get the current power levels of this room.
420    pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
421        let power_levels_content = self
422            .store
423            .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
424            .await?
425            .ok_or(Error::InsufficientData)?
426            .deserialize()?;
427        let creators = self.creators().ok_or(Error::InsufficientData)?;
428        let rules = self.info.read().room_version_rules_or_default();
429
430        Ok(power_levels_content.power_levels(&rules.authorization, creators))
431    }
432
433    /// Get the current power levels of this room, or a sensible default if they
434    /// are not known.
435    pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
436        if let Ok(power_levels) = self.power_levels().await {
437            return power_levels;
438        }
439
440        // As a fallback, create the default power levels of a room.
441        let rules = self.info.read().room_version_rules_or_default();
442        RoomPowerLevels::new(
443            RoomPowerLevelsSource::None,
444            &rules.authorization,
445            self.creators().into_iter().flatten(),
446        )
447    }
448
449    /// Get the `m.room.name` of this room.
450    ///
451    /// The returned string may be empty if the event has been redacted, or it's
452    /// missing from storage.
453    pub fn name(&self) -> Option<String> {
454        self.info.read().name().map(ToOwned::to_owned)
455    }
456
457    /// Get the topic of the room.
458    pub fn topic(&self) -> Option<String> {
459        self.info.read().topic().map(ToOwned::to_owned)
460    }
461
462    /// Update the cached user defined notification mode.
463    ///
464    /// This is automatically recomputed on every successful sync, and the
465    /// cached result can be retrieved in
466    /// [`Self::cached_user_defined_notification_mode`].
467    pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
468        self.info.update_if(|info| {
469            if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
470                info.cached_user_defined_notification_mode = Some(mode);
471
472                true
473            } else {
474                false
475            }
476        });
477    }
478
479    /// Returns the cached user defined notification mode, if available.
480    ///
481    /// This cache is refilled every time we call
482    /// [`Self::update_cached_user_defined_notification_mode`].
483    pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
484        self.info.read().cached_user_defined_notification_mode
485    }
486
487    /// Removes any existing cached value for the user defined notification
488    /// mode.
489    pub fn clear_user_defined_notification_mode(&self) {
490        self.info.update_if(|info| {
491            if info.cached_user_defined_notification_mode.is_some() {
492                info.cached_user_defined_notification_mode = None;
493                true
494            } else {
495                false
496            }
497        })
498    }
499
500    /// Get the list of users ids that are considered to be joined members of
501    /// this room.
502    pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
503        self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
504    }
505
506    /// The user IDs of this room's heroes, as stored, for cheaply checking hero
507    /// membership without loading their global profiles.
508    #[cfg(feature = "unstable-msc4426")]
509    pub(crate) fn hero_user_ids(&self) -> Vec<OwnedUserId> {
510        self.info.read().heroes().iter().map(|hero| hero.user_id.clone()).collect()
511    }
512
513    /// Get the heroes for this room.
514    ///
515    /// This also filters out possible service members from the list of heroes
516    /// returned by the homeserver.
517    #[cfg_attr(not(feature = "unstable-msc4426"), allow(clippy::unused_async))]
518    pub async fn heroes(&self) -> Vec<RoomHeroWithProfile> {
519        let heroes: Vec<RoomHero> = {
520            let guard = self.info.read();
521            let heroes = guard.heroes();
522
523            if let Some(service_members) = guard.service_members() {
524                heroes
525                    .iter()
526                    .filter(|hero| !service_members.contains(&hero.user_id))
527                    .cloned()
528                    .collect()
529            } else {
530                heroes.to_vec()
531            }
532        };
533
534        // Short-circuiting if there is no heroes: we can't do anything.
535        if heroes.is_empty() {
536            return Vec::new();
537        }
538
539        // Return with empty profile fields when the user status feature is
540        // disabled.
541        #[cfg(not(feature = "unstable-msc4426"))]
542        {
543            heroes.into_iter().map(RoomHeroWithProfile::from).collect()
544        }
545
546        // Merge any fields from the user's persisted global profile.
547        #[cfg(feature = "unstable-msc4426")]
548        {
549            let user_ids = heroes.iter().map(|hero| hero.user_id.clone()).collect::<Vec<_>>();
550
551            let mut global_profiles =
552                self.store.get_global_profiles(&user_ids).await.unwrap_or_else(|error| {
553                    tracing::warn!(?error, "Failed to load global profiles for room heroes");
554                    Default::default()
555                });
556
557            heroes
558                .into_iter()
559                .map(|hero| {
560                    let (status, call) = global_profiles
561                        .remove(&*hero.user_id)
562                        .map(|profile| {
563                            (
564                                profile.get_static::<Status>().ok().flatten(),
565                                profile.get_static::<Call>().ok().flatten(),
566                            )
567                        })
568                        .unwrap_or_default();
569
570                    RoomHeroWithProfile {
571                        user_id: hero.user_id,
572                        display_name: hero.display_name,
573                        avatar_url: hero.avatar_url,
574                        status,
575                        call,
576                    }
577                })
578                .collect()
579        }
580    }
581
582    /// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
583    /// `receipt_type`, `thread` and `user_id` in this room.
584    pub async fn load_user_receipt(
585        &self,
586        receipt_type: ReceiptType,
587        receipt_thread: &ReceiptThread,
588        user_id: &UserId,
589    ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
590        self.store
591            .get_user_room_receipt_event(self.room_id(), receipt_type, receipt_thread, user_id)
592            .await
593    }
594
595    /// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
596    /// tuples for the given `receipt_type`, `thread` and `event_id` in this
597    /// room.
598    pub async fn load_event_receipts(
599        &self,
600        receipt_type: ReceiptType,
601        receipt_thread: &ReceiptThread,
602        event_id: &EventId,
603    ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
604        self.store
605            .get_event_room_receipt_events(self.room_id(), receipt_type, receipt_thread, event_id)
606            .await
607    }
608
609    /// Returns a boolean indicating if this room has been manually marked as
610    /// unread
611    pub fn is_marked_unread(&self) -> bool {
612        self.info.read().base_info.is_marked_unread
613    }
614
615    /// Returns the event ID of the user's `m.fully_read` marker for this room,
616    /// if any.
617    pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
618        self.info.read().fully_read_event_id().map(ToOwned::to_owned)
619    }
620
621    /// Returns the [`RoomVersionId`] of the room, if known.
622    pub fn version(&self) -> Option<RoomVersionId> {
623        self.info.read().room_version().cloned()
624    }
625
626    /// Returns the recency stamp of the room.
627    ///
628    /// Please read `RoomInfo::recency_stamp` to learn more.
629    pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
630        self.info.read().recency_stamp
631    }
632
633    /// Get a `Stream` of loaded pinned events for this room. If no pinned
634    /// events are found a single empty `Vec` will be returned.
635    pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
636        self.info
637            .subscribe()
638            .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
639    }
640
641    /// Returns the current pinned event ids for this room.
642    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
643        self.info.read().pinned_event_ids()
644    }
645
646    /// Computes and stores the list of service members that are either in a
647    /// joined or invited state in this room, checking the service member list
648    /// against the locally available room members.
649    pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
650        if let Some(service_members) = self.service_members() {
651            let mut found = Vec::new();
652            for user_id in service_members {
653                match self.get_member(&user_id).await {
654                    Ok(Some(member)) => {
655                        // We only care about active members (joined or invited)
656                        if matches!(
657                            member.membership(),
658                            MembershipState::Join | MembershipState::Invite
659                        ) {
660                            found.push(member);
661                        }
662                    }
663                    Ok(None) => (),
664                    Err(error) => return Err(error),
665                }
666            }
667
668            trace!("Updating active service members ({}) in room {}", found.len(), self.room_id());
669
670            let new_active_service_member_count = found.len() as u64;
671            let current_active_service_member_count =
672                self.info.read().summary.active_service_members.unwrap_or_default();
673            if new_active_service_member_count != current_active_service_member_count {
674                self.update_and_save_room_info(|mut info| {
675                    info.update_active_service_member_count(Some(new_active_service_member_count));
676                    (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
677                })
678                .await?;
679            }
680
681            Ok(Some(found))
682        } else {
683            if self.info.read().summary.active_service_members.is_some() {
684                self.update_and_save_room_info(|mut info| {
685                    info.update_active_service_member_count(None);
686                    (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
687                })
688                .await?;
689            }
690            Ok(None)
691        }
692    }
693
694    /// Computes the joined service members in this room.
695    ///
696    /// This result is useful for computing a room's display name, i.e.
697    #[instrument(skip_all, fields(room_id = ?self.room_id))]
698    pub async fn compute_joined_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
699        if !self.are_members_synced() {
700            trace!("Tried to compute joined service members in a room that is not synced");
701            return Ok(None);
702        }
703        if let Some(service_member_ids) = self.service_members() {
704            let mut ret = vec![];
705            for user_id in service_member_ids.iter() {
706                if let Some(member) = self.get_member(user_id).await.unwrap()
707                    && matches!(member.membership(), MembershipState::Join)
708                {
709                    trace!("Found a joined service member ({})", user_id);
710                    ret.push(member);
711                } else {
712                    trace!("Did not find a joined service member ({})", user_id);
713                }
714            }
715            trace!(
716                "Computed joined service members ({}) for service member count {}",
717                ret.len(),
718                service_member_ids.len()
719            );
720            Ok(Some(ret))
721        } else {
722            trace!("Tried to compute joined service members in a room that has no service members",);
723            Ok(None)
724        }
725    }
726
727    /// Returns a cached value containing the active (joined/invited) service
728    /// member count, if known.
729    pub fn active_service_members_count(&self) -> Option<u64> {
730        self.info.read().summary.active_service_members
731    }
732}
733
734// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
735#[cfg(not(feature = "test-send-sync"))]
736unsafe impl Send for Room {}
737
738// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
739#[cfg(not(feature = "test-send-sync"))]
740unsafe impl Sync for Room {}
741
742#[cfg(feature = "test-send-sync")]
743#[test]
744// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
745fn test_send_sync_for_room() {
746    fn assert_send_sync<
747        T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
748    >() {
749    }
750
751    assert_send_sync::<Room>();
752}
753
754/// The possible sources of an account data type.
755#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
756pub(crate) enum AccountDataSource {
757    /// The source is account data with the stable prefix.
758    Stable,
759
760    /// The source is account data with the unstable prefix.
761    #[default]
762    Unstable,
763}
764
765#[cfg(test)]
766mod tests {
767    use matrix_sdk_test::{
768        JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
769    };
770    use ruma::{room_id, user_id};
771    use serde_json::json;
772
773    use super::*;
774    use crate::test_utils::logged_in_base_client;
775
776    #[async_test]
777    async fn test_room_heroes_filters_out_service_members() {
778        let client = logged_in_base_client(None).await;
779        let user_id = &client.session_meta().unwrap().user_id;
780        let service_member_id = user_id!("@service:example.org");
781        let alice_id = user_id!("@alice:example.org");
782        let room_id = room_id!("!room:example.org");
783
784        let room = client.get_or_create_room(room_id, RoomState::Joined);
785
786        // Create a room response with 2 heroes, one of them a service member.
787        let mut sync_builder = SyncResponseBuilder::new();
788        let response = sync_builder
789            .add_joined_room(
790                JoinedRoomBuilder::new(room_id)
791                    .set_room_summary(json!({
792                        "m.joined_member_count": 3,
793                        "m.invited_member_count": 0,
794                        "m.heroes": [alice_id.to_owned(), service_member_id.to_owned()],
795                    }))
796                    .add_state_event(
797                        EventFactory::new()
798                            .sender(user_id)
799                            .member_hints(BTreeSet::from([service_member_id.to_owned()])),
800                    ),
801            )
802            .build_sync_response();
803
804        client.receive_sync_response(response).await.unwrap();
805
806        // The service member should be filtered out.
807        let heroes = room.heroes().await;
808        assert_eq!(heroes.len(), 1);
809        assert_eq!(heroes[0].user_id, alice_id);
810    }
811
812    #[async_test]
813    async fn test_human_member_ids_filters_out_service_members() {
814        let client = logged_in_base_client(None).await;
815        let user_id = &client.session_meta().unwrap().user_id;
816        let service_member_id = user_id!("@service:example.org");
817        let alice_id = user_id!("@alice:example.org");
818        let bob_id = user_id!("@bob:example.org");
819        let room_id = room_id!("!room:example.org");
820
821        let room = client.get_or_create_room(room_id, RoomState::Joined);
822        let factory = EventFactory::new().room(room_id);
823
824        let service_member_hint =
825            factory.member_hints(BTreeSet::from([service_member_id.to_owned()])).sender(user_id);
826        let alice_joins = factory.member(alice_id);
827        let service_member_joins = factory.member(service_member_id);
828        let bob_leaves = factory.member(bob_id).membership(MembershipState::Leave);
829
830        let mut sync_builder = SyncResponseBuilder::new();
831        let response = sync_builder
832            .add_joined_room(
833                JoinedRoomBuilder::new(room_id)
834                    .add_state_event(service_member_hint)
835                    .add_state_event(alice_joins)
836                    .add_state_event(service_member_joins)
837                    .add_state_event(bob_leaves),
838            )
839            .build_sync_response();
840
841        client.receive_sync_response(response).await.unwrap();
842
843        assert_eq!(
844            room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
845            vec![alice_id.to_owned()]
846        );
847        assert_eq!(
848            room.human_member_ids(RoomMemberships::empty())
849                .await
850                .unwrap()
851                .into_iter()
852                .collect::<BTreeSet<_>>(),
853            BTreeSet::from([alice_id.to_owned(), bob_id.to_owned()])
854        );
855    }
856
857    #[async_test]
858    async fn test_human_member_ids_without_member_hints() {
859        let client = logged_in_base_client(None).await;
860        let alice_id = user_id!("@alice:example.org");
861        let room_id = room_id!("!room:example.org");
862
863        let room = client.get_or_create_room(room_id, RoomState::Joined);
864        let factory = EventFactory::new().room(room_id);
865
866        let alice_joins = factory.member(alice_id);
867
868        let mut sync_builder = SyncResponseBuilder::new();
869        let response = sync_builder
870            .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(alice_joins))
871            .build_sync_response();
872
873        client.receive_sync_response(response).await.unwrap();
874
875        assert_eq!(
876            room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
877            vec![alice_id.to_owned()]
878        );
879    }
880
881    #[cfg(feature = "unstable-msc4426")]
882    #[async_test]
883    async fn test_room_heroes_carry_global_profile() {
884        use ruma::{
885            SecondsSinceUnixEpoch,
886            profile::{
887                CallProfileField, ProfileFieldValue, StatusProfileField, UserProfileChanges,
888                UserProfileUpdate,
889            },
890        };
891
892        use crate::store::StateChanges;
893
894        let client = logged_in_base_client(None).await;
895        let alice_id = user_id!("@alice:example.org");
896        let room_id = room_id!("!room:example.org");
897
898        let room = client.get_or_create_room(room_id, RoomState::Joined);
899
900        let mut sync_builder = SyncResponseBuilder::new();
901        let response = sync_builder
902            .add_joined_room(JoinedRoomBuilder::new(room_id).set_room_summary(json!({
903                "m.joined_member_count": 2,
904                "m.invited_member_count": 0,
905                "m.heroes": [alice_id.to_owned()],
906            })))
907            .build_sync_response();
908        client.receive_sync_response(response).await.unwrap();
909
910        // Without a stored global profile, the hero carries no status or call.
911        let heroes = room.heroes().await;
912        assert_eq!(heroes.len(), 1);
913        assert_eq!(heroes[0].user_id, alice_id);
914        assert!(heroes[0].status.is_none());
915        assert!(heroes[0].call.is_none());
916
917        // Store a global profile carrying an `m.status` and `m.call` for the
918        // hero.
919        let mut call = CallProfileField::new();
920        call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
921        let mut changes = StateChanges::default();
922        changes.global_profiles.insert(alice_id.to_owned(), {
923            let mut profile_changes = UserProfileChanges::new();
924            profile_changes.insert_updated_value(ProfileFieldValue::Status(
925                StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
926            ));
927            profile_changes.insert_updated_value(ProfileFieldValue::Call(call));
928            UserProfileUpdate::Updated(profile_changes)
929        });
930        client.state_store().save_changes(&changes).await.unwrap();
931
932        // The hero now surfaces the status and call from the global profile.
933        let heroes = room.heroes().await;
934        let hero = &heroes[0];
935        let status = hero.status.as_ref().expect("status is set");
936        assert_eq!(status.text, "Working");
937        assert_eq!(status.emoji, "💻");
938        assert_eq!(
939            hero.call.as_ref().expect("call is set").call_joined_ts,
940            Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
941        );
942    }
943}