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            retention::RoomRetentionEventContent,
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, trace, warn};
70
71use crate::{
72    DmRoomDefinition, Error, StateStore,
73    deserialized_responses::MemberEvent,
74    notification_settings::RoomNotificationMode,
75    read_receipts::ReadReceipts,
76    store::{Result as StoreResult, SaveLockedStateStore, 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: SaveLockedStateStore,
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: SaveLockedStateStore,
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: SaveLockedStateStore,
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) -> ReadReceipts {
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    /// Computes if the current room is a DM based on the rules from the
308    /// [`DmRoomDefinition`], updating the active service members.
309    pub async fn compute_is_dm(&self, dm_room_definition: &DmRoomDefinition) -> StoreResult<bool> {
310        let is_direct = self.is_direct().await?;
311
312        match *dm_room_definition {
313            DmRoomDefinition::MatrixSpec => Ok(is_direct),
314            DmRoomDefinition::TwoMembers => {
315                if !is_direct {
316                    return Ok(false);
317                }
318                let active_service_member_count =
319                    self.update_active_service_members().await?.unwrap_or_default().len() as u64;
320                let has_at_most_two_members =
321                    self.active_members_count().saturating_sub(active_service_member_count) <= 2;
322                Ok(has_at_most_two_members)
323            }
324        }
325    }
326
327    /// If this room is a direct message, get the members that we're sharing the
328    /// room with.
329    ///
330    /// *Note*: The member list might have been modified in the meantime and
331    /// the targets might not even be in the room anymore. This setting should
332    /// only be considered as guidance. We leave members in this list to allow
333    /// us to re-find a DM with a user even if they have left, since we may
334    /// want to re-invite them.
335    pub fn direct_targets(&self) -> HashSet<OwnedDirectUserIdentifier> {
336        self.info.read().base_info.dm_targets.clone()
337    }
338
339    /// If this room is a direct message, returns the number of members that
340    /// we're sharing the room with.
341    pub fn direct_targets_length(&self) -> usize {
342        self.info.read().base_info.dm_targets.len()
343    }
344
345    /// Get the guest access policy of this room.
346    pub fn guest_access(&self) -> GuestAccess {
347        self.info.read().guest_access().clone()
348    }
349
350    /// Get the history visibility policy of this room.
351    pub fn history_visibility(&self) -> Option<HistoryVisibility> {
352        self.info.read().history_visibility().cloned()
353    }
354
355    /// Get the history visibility policy of this room, or a sensible default if
356    /// the event is missing.
357    pub fn history_visibility_or_default(&self) -> HistoryVisibility {
358        self.info.read().history_visibility_or_default().clone()
359    }
360
361    /// Is the room considered to be public.
362    ///
363    /// May return `None` if the join rule event is not available.
364    pub fn is_public(&self) -> Option<bool> {
365        self.info.read().join_rule().map(|join_rule| matches!(join_rule, JoinRule::Public))
366    }
367
368    /// Get the join rule policy of this room, if available.
369    pub fn join_rule(&self) -> Option<JoinRule> {
370        self.info.read().join_rule().cloned()
371    }
372
373    /// Get the maximum power level that this room contains.
374    ///
375    /// This is useful if one wishes to normalize the power levels, e.g. from
376    /// 0-100 where 100 would be the max power level.
377    pub fn max_power_level(&self) -> i64 {
378        self.info.read().base_info.max_power_level
379    }
380
381    /// Get the message retention policy of this room, if set.
382    pub fn retention(&self) -> Option<RoomRetentionEventContent> {
383        self.info.read().retention().cloned()
384    }
385
386    /// Get the service members in this room, if available.
387    pub fn service_members(&self) -> Option<BTreeSet<OwnedUserId>> {
388        self.info.read().service_members().cloned()
389    }
390
391    /// Get the current power levels of this room.
392    pub async fn power_levels(&self) -> Result<RoomPowerLevels, Error> {
393        let power_levels_content = self
394            .store
395            .get_state_event_static::<RoomPowerLevelsEventContent>(self.room_id())
396            .await?
397            .ok_or(Error::InsufficientData)?
398            .deserialize()?;
399        let creators = self.creators().ok_or(Error::InsufficientData)?;
400        let rules = self.info.read().room_version_rules_or_default();
401
402        Ok(power_levels_content.power_levels(&rules.authorization, creators))
403    }
404
405    /// Get the current power levels of this room, or a sensible default if they
406    /// are not known.
407    pub async fn power_levels_or_default(&self) -> RoomPowerLevels {
408        if let Ok(power_levels) = self.power_levels().await {
409            return power_levels;
410        }
411
412        // As a fallback, create the default power levels of a room.
413        let rules = self.info.read().room_version_rules_or_default();
414        RoomPowerLevels::new(
415            RoomPowerLevelsSource::None,
416            &rules.authorization,
417            self.creators().into_iter().flatten(),
418        )
419    }
420
421    /// Get the `m.room.name` of this room.
422    ///
423    /// The returned string may be empty if the event has been redacted, or it's
424    /// missing from storage.
425    pub fn name(&self) -> Option<String> {
426        self.info.read().name().map(ToOwned::to_owned)
427    }
428
429    /// Get the topic of the room.
430    pub fn topic(&self) -> Option<String> {
431        self.info.read().topic().map(ToOwned::to_owned)
432    }
433
434    /// Update the cached user defined notification mode.
435    ///
436    /// This is automatically recomputed on every successful sync, and the
437    /// cached result can be retrieved in
438    /// [`Self::cached_user_defined_notification_mode`].
439    pub fn update_cached_user_defined_notification_mode(&self, mode: RoomNotificationMode) {
440        self.info.update_if(|info| {
441            if info.cached_user_defined_notification_mode.as_ref() != Some(&mode) {
442                info.cached_user_defined_notification_mode = Some(mode);
443
444                true
445            } else {
446                false
447            }
448        });
449    }
450
451    /// Returns the cached user defined notification mode, if available.
452    ///
453    /// This cache is refilled every time we call
454    /// [`Self::update_cached_user_defined_notification_mode`].
455    pub fn cached_user_defined_notification_mode(&self) -> Option<RoomNotificationMode> {
456        self.info.read().cached_user_defined_notification_mode
457    }
458
459    /// Removes any existing cached value for the user defined notification
460    /// mode.
461    pub fn clear_user_defined_notification_mode(&self) {
462        self.info.update_if(|info| {
463            if info.cached_user_defined_notification_mode.is_some() {
464                info.cached_user_defined_notification_mode = None;
465                true
466            } else {
467                false
468            }
469        })
470    }
471
472    /// Get the list of users ids that are considered to be joined members of
473    /// this room.
474    pub async fn joined_user_ids(&self) -> StoreResult<Vec<OwnedUserId>> {
475        self.store.get_user_ids(self.room_id(), RoomMemberships::JOIN).await
476    }
477
478    /// The user IDs of this room's heroes, as stored, for cheaply checking
479    /// hero membership without loading their global profiles.
480    #[cfg(feature = "unstable-msc4426")]
481    pub(crate) fn hero_user_ids(&self) -> Vec<OwnedUserId> {
482        self.info.read().heroes().iter().map(|hero| hero.user_id.clone()).collect()
483    }
484
485    /// Get the heroes for this room.
486    ///
487    /// This also filters out possible service members from the list of heroes
488    /// returned by the homeserver.
489    #[cfg_attr(not(feature = "unstable-msc4426"), allow(clippy::unused_async))]
490    pub async fn heroes(&self) -> Vec<RoomHeroWithProfile> {
491        let heroes: Vec<RoomHero> = {
492            let guard = self.info.read();
493            let heroes = guard.heroes();
494
495            if let Some(service_members) = guard.service_members() {
496                heroes
497                    .iter()
498                    .filter(|hero| !service_members.contains(&hero.user_id))
499                    .cloned()
500                    .collect()
501            } else {
502                heroes.to_vec()
503            }
504        };
505
506        // Short-circuiting if there is no heroes: we can't do anything.
507        if heroes.is_empty() {
508            return Vec::new();
509        }
510
511        // Return with empty profile fields when the user status feature is disabled.
512        #[cfg(not(feature = "unstable-msc4426"))]
513        {
514            heroes.into_iter().map(RoomHeroWithProfile::from).collect()
515        }
516
517        // Merge any fields from the user's persisted global profile.
518        #[cfg(feature = "unstable-msc4426")]
519        {
520            let user_ids = heroes.iter().map(|hero| hero.user_id.clone()).collect::<Vec<_>>();
521
522            let mut global_profiles =
523                self.store.get_global_profiles(&user_ids).await.unwrap_or_else(|error| {
524                    tracing::warn!(?error, "Failed to load global profiles for room heroes");
525                    Default::default()
526                });
527
528            heroes
529                .into_iter()
530                .map(|hero| {
531                    let (status, call) = global_profiles
532                        .remove(&*hero.user_id)
533                        .map(|profile| {
534                            (
535                                profile.get_static::<Status>().ok().flatten(),
536                                profile.get_static::<Call>().ok().flatten(),
537                            )
538                        })
539                        .unwrap_or_default();
540
541                    RoomHeroWithProfile {
542                        user_id: hero.user_id,
543                        display_name: hero.display_name,
544                        avatar_url: hero.avatar_url,
545                        status,
546                        call,
547                    }
548                })
549                .collect()
550        }
551    }
552
553    /// Get the receipt as an `OwnedEventId` and `Receipt` tuple for the given
554    /// `receipt_type`, `thread` and `user_id` in this room.
555    pub async fn load_user_receipt(
556        &self,
557        receipt_type: ReceiptType,
558        receipt_thread: &ReceiptThread,
559        user_id: &UserId,
560    ) -> StoreResult<Option<(OwnedEventId, Receipt)>> {
561        self.store
562            .get_user_room_receipt_event(self.room_id(), receipt_type, receipt_thread, user_id)
563            .await
564    }
565
566    /// Load from storage the receipts as a list of `OwnedUserId` and `Receipt`
567    /// tuples for the given `receipt_type`, `thread` and `event_id` in this
568    /// room.
569    pub async fn load_event_receipts(
570        &self,
571        receipt_type: ReceiptType,
572        receipt_thread: &ReceiptThread,
573        event_id: &EventId,
574    ) -> StoreResult<Vec<(OwnedUserId, Receipt)>> {
575        self.store
576            .get_event_room_receipt_events(self.room_id(), receipt_type, receipt_thread, event_id)
577            .await
578    }
579
580    /// Returns a boolean indicating if this room has been manually marked as
581    /// unread
582    pub fn is_marked_unread(&self) -> bool {
583        self.info.read().base_info.is_marked_unread
584    }
585
586    /// Returns the event ID of the user's `m.fully_read` marker for this room,
587    /// if any.
588    pub fn fully_read_event_id(&self) -> Option<OwnedEventId> {
589        self.info.read().fully_read_event_id().map(ToOwned::to_owned)
590    }
591
592    /// Returns the [`RoomVersionId`] of the room, if known.
593    pub fn version(&self) -> Option<RoomVersionId> {
594        self.info.read().room_version().cloned()
595    }
596
597    /// Returns the recency stamp of the room.
598    ///
599    /// Please read `RoomInfo::recency_stamp` to learn more.
600    pub fn recency_stamp(&self) -> Option<RoomRecencyStamp> {
601        self.info.read().recency_stamp
602    }
603
604    /// Get a `Stream` of loaded pinned events for this room.
605    /// If no pinned events are found a single empty `Vec` will be returned.
606    pub fn pinned_event_ids_stream(&self) -> impl Stream<Item = Vec<OwnedEventId>> + use<> {
607        self.info
608            .subscribe()
609            .map(|i| i.base_info.pinned_events.and_then(|c| c.pinned).unwrap_or_default())
610    }
611
612    /// Returns the current pinned event ids for this room.
613    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
614        self.info.read().pinned_event_ids()
615    }
616
617    /// Computes and stores the list of service members that are either in a
618    /// joined or invited state in this room, checking the service member
619    /// list against the locally available room members.
620    pub async fn update_active_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
621        if let Some(service_members) = self.service_members() {
622            let mut found = Vec::new();
623            for user_id in service_members {
624                match self.get_member(&user_id).await {
625                    Ok(Some(member)) => {
626                        // We only care about active members (joined or invited)
627                        if matches!(
628                            member.membership(),
629                            MembershipState::Join | MembershipState::Invite
630                        ) {
631                            found.push(member);
632                        }
633                    }
634                    Ok(None) => (),
635                    Err(error) => return Err(error),
636                }
637            }
638
639            trace!("Updating active service members ({}) in room {}", found.len(), self.room_id());
640
641            let new_active_service_member_count = found.len() as u64;
642            let current_active_service_member_count =
643                self.info.read().summary.active_service_members.unwrap_or_default();
644            if new_active_service_member_count != current_active_service_member_count {
645                self.update_and_save_room_info(|mut info| {
646                    info.update_active_service_member_count(Some(new_active_service_member_count));
647                    (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
648                })
649                .await?;
650            }
651
652            Ok(Some(found))
653        } else {
654            if self.info.read().summary.active_service_members.is_some() {
655                self.update_and_save_room_info(|mut info| {
656                    info.update_active_service_member_count(None);
657                    (info, RoomInfoNotableUpdateReasons::ACTIVE_SERVICE_MEMBERS)
658                })
659                .await?;
660            }
661            Ok(None)
662        }
663    }
664
665    /// Computes the joined service members in this room.
666    ///
667    /// This result is useful for computing a room's display name, i.e.
668    #[instrument(skip_all, fields(room_id = ?self.room_id))]
669    pub async fn compute_joined_service_members(&self) -> StoreResult<Option<Vec<RoomMember>>> {
670        if !self.are_members_synced() {
671            trace!("Tried to compute joined service members in a room that is not synced");
672            return Ok(None);
673        }
674        if let Some(service_member_ids) = self.service_members() {
675            let mut ret = vec![];
676            for user_id in service_member_ids.iter() {
677                if let Some(member) = self.get_member(user_id).await.unwrap()
678                    && matches!(member.membership(), MembershipState::Join)
679                {
680                    trace!("Found a joined service member ({})", user_id);
681                    ret.push(member);
682                } else {
683                    trace!("Did not find a joined service member ({})", user_id);
684                }
685            }
686            trace!(
687                "Computed joined service members ({}) for service member count {}",
688                ret.len(),
689                service_member_ids.len()
690            );
691            Ok(Some(ret))
692        } else {
693            trace!("Tried to compute joined service members in a room that has no service members",);
694            Ok(None)
695        }
696    }
697
698    /// Returns a cached value containing the active (joined/invited) service
699    /// member count, if known.
700    pub fn active_service_members_count(&self) -> Option<u64> {
701        self.info.read().summary.active_service_members
702    }
703}
704
705// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
706#[cfg(not(feature = "test-send-sync"))]
707unsafe impl Send for Room {}
708
709// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
710#[cfg(not(feature = "test-send-sync"))]
711unsafe impl Sync for Room {}
712
713#[cfg(feature = "test-send-sync")]
714#[test]
715// See https://github.com/matrix-org/matrix-rust-sdk/pull/3749#issuecomment-2312939823.
716fn test_send_sync_for_room() {
717    fn assert_send_sync<
718        T: matrix_sdk_common::SendOutsideWasm + matrix_sdk_common::SyncOutsideWasm,
719    >() {
720    }
721
722    assert_send_sync::<Room>();
723}
724
725/// The possible sources of an account data type.
726#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
727pub(crate) enum AccountDataSource {
728    /// The source is account data with the stable prefix.
729    Stable,
730
731    /// The source is account data with the unstable prefix.
732    #[default]
733    Unstable,
734}
735
736#[cfg(test)]
737mod tests {
738    use matrix_sdk_test::{
739        JoinedRoomBuilder, SyncResponseBuilder, async_test, event_factory::EventFactory,
740    };
741    use ruma::{room_id, user_id};
742    use serde_json::json;
743
744    use super::*;
745    use crate::test_utils::logged_in_base_client;
746
747    #[async_test]
748    async fn test_room_heroes_filters_out_service_members() {
749        let client = logged_in_base_client(None).await;
750        let user_id = &client.session_meta().unwrap().user_id;
751        let service_member_id = user_id!("@service:example.org");
752        let alice_id = user_id!("@alice:example.org");
753        let room_id = room_id!("!room:example.org");
754
755        let room = client.get_or_create_room(room_id, RoomState::Joined);
756
757        // Create a room response with 2 heroes, one of them a service member.
758        let mut sync_builder = SyncResponseBuilder::new();
759        let response = sync_builder
760            .add_joined_room(
761                JoinedRoomBuilder::new(room_id)
762                    .set_room_summary(json!({
763                        "m.joined_member_count": 3,
764                        "m.invited_member_count": 0,
765                        "m.heroes": [alice_id.to_owned(), service_member_id.to_owned()],
766                    }))
767                    .add_state_event(
768                        EventFactory::new()
769                            .sender(user_id)
770                            .member_hints(BTreeSet::from([service_member_id.to_owned()])),
771                    ),
772            )
773            .build_sync_response();
774
775        client.receive_sync_response(response).await.unwrap();
776
777        // The service member should be filtered out.
778        let heroes = room.heroes().await;
779        assert_eq!(heroes.len(), 1);
780        assert_eq!(heroes[0].user_id, alice_id);
781    }
782
783    #[async_test]
784    async fn test_human_member_ids_filters_out_service_members() {
785        let client = logged_in_base_client(None).await;
786        let user_id = &client.session_meta().unwrap().user_id;
787        let service_member_id = user_id!("@service:example.org");
788        let alice_id = user_id!("@alice:example.org");
789        let bob_id = user_id!("@bob: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        let factory = EventFactory::new().room(room_id);
794
795        let service_member_hint =
796            factory.member_hints(BTreeSet::from([service_member_id.to_owned()])).sender(user_id);
797        let alice_joins = factory.member(alice_id);
798        let service_member_joins = factory.member(service_member_id);
799        let bob_leaves = factory.member(bob_id).membership(MembershipState::Leave);
800
801        let mut sync_builder = SyncResponseBuilder::new();
802        let response = sync_builder
803            .add_joined_room(
804                JoinedRoomBuilder::new(room_id)
805                    .add_state_event(service_member_hint)
806                    .add_state_event(alice_joins)
807                    .add_state_event(service_member_joins)
808                    .add_state_event(bob_leaves),
809            )
810            .build_sync_response();
811
812        client.receive_sync_response(response).await.unwrap();
813
814        assert_eq!(
815            room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
816            vec![alice_id.to_owned()]
817        );
818        assert_eq!(
819            room.human_member_ids(RoomMemberships::empty())
820                .await
821                .unwrap()
822                .into_iter()
823                .collect::<BTreeSet<_>>(),
824            BTreeSet::from([alice_id.to_owned(), bob_id.to_owned()])
825        );
826    }
827
828    #[async_test]
829    async fn test_human_member_ids_without_member_hints() {
830        let client = logged_in_base_client(None).await;
831        let alice_id = user_id!("@alice:example.org");
832        let room_id = room_id!("!room:example.org");
833
834        let room = client.get_or_create_room(room_id, RoomState::Joined);
835        let factory = EventFactory::new().room(room_id);
836
837        let alice_joins = factory.member(alice_id);
838
839        let mut sync_builder = SyncResponseBuilder::new();
840        let response = sync_builder
841            .add_joined_room(JoinedRoomBuilder::new(room_id).add_state_event(alice_joins))
842            .build_sync_response();
843
844        client.receive_sync_response(response).await.unwrap();
845
846        assert_eq!(
847            room.human_member_ids(RoomMemberships::ACTIVE).await.unwrap(),
848            vec![alice_id.to_owned()]
849        );
850    }
851
852    #[cfg(feature = "unstable-msc4426")]
853    #[async_test]
854    async fn test_room_heroes_carry_global_profile() {
855        use ruma::{
856            SecondsSinceUnixEpoch,
857            profile::{
858                CallProfileField, ProfileFieldValue, StatusProfileField, UserProfileChanges,
859                UserProfileUpdate,
860            },
861        };
862
863        use crate::store::StateChanges;
864
865        let client = logged_in_base_client(None).await;
866        let alice_id = user_id!("@alice:example.org");
867        let room_id = room_id!("!room:example.org");
868
869        let room = client.get_or_create_room(room_id, RoomState::Joined);
870
871        let mut sync_builder = SyncResponseBuilder::new();
872        let response = sync_builder
873            .add_joined_room(JoinedRoomBuilder::new(room_id).set_room_summary(json!({
874                "m.joined_member_count": 2,
875                "m.invited_member_count": 0,
876                "m.heroes": [alice_id.to_owned()],
877            })))
878            .build_sync_response();
879        client.receive_sync_response(response).await.unwrap();
880
881        // Without a stored global profile, the hero carries no status or call.
882        let heroes = room.heroes().await;
883        assert_eq!(heroes.len(), 1);
884        assert_eq!(heroes[0].user_id, alice_id);
885        assert!(heroes[0].status.is_none());
886        assert!(heroes[0].call.is_none());
887
888        // Store a global profile carrying an `m.status` and `m.call` for the hero.
889        let mut call = CallProfileField::new();
890        call.call_joined_ts = Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()));
891        let mut changes = StateChanges::default();
892        changes.global_profiles.insert(alice_id.to_owned(), {
893            let mut profile_changes = UserProfileChanges::new();
894            profile_changes.insert_updated_value(ProfileFieldValue::Status(
895                StatusProfileField::new("Working".to_owned(), "💻".to_owned()),
896            ));
897            profile_changes.insert_updated_value(ProfileFieldValue::Call(call));
898            UserProfileUpdate::Updated(profile_changes)
899        });
900        client.state_store().save_changes(&changes).await.unwrap();
901
902        // The hero now surfaces the status and call from the global profile.
903        let heroes = room.heroes().await;
904        let hero = &heroes[0];
905        let status = hero.status.as_ref().expect("status is set");
906        assert_eq!(status.text, "Working");
907        assert_eq!(status.emoji, "💻");
908        assert_eq!(
909            hero.call.as_ref().expect("call is set").call_joined_ts,
910            Some(SecondsSinceUnixEpoch(1_700_000_000u32.into()))
911        );
912    }
913}