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