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