Skip to main content

matrix_sdk_base/room/
room_info.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
15use std::{
16    collections::{BTreeMap, BTreeSet, HashSet},
17    sync::{Arc, atomic::AtomicBool},
18};
19
20use as_variant::as_variant;
21use bitflags::bitflags;
22use eyeball::Subscriber;
23use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
24use ruma::{
25    EventId, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId, OwnedRoomId, OwnedUserId,
26    RoomAliasId, RoomId, RoomVersionId,
27    api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
28    events::{
29        AnyPossiblyRedactedStateEventContent, AnyStrippedStateEvent, AnySyncStateEvent,
30        AnySyncTimelineEvent, StateEventType,
31        call::member::{
32            CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
33        },
34        direct::OwnedDirectUserIdentifier,
35        member_hints::PossiblyRedactedMemberHintsEventContent,
36        room::{
37            avatar::{self, PossiblyRedactedRoomAvatarEventContent},
38            canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
39            encryption::PossiblyRedactedRoomEncryptionEventContent,
40            guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
41            history_visibility::{
42                HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
43            },
44            join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
45            name::PossiblyRedactedRoomNameEventContent,
46            pinned_events::{
47                PossiblyRedactedRoomPinnedEventsEventContent, RoomPinnedEventsEventContent,
48            },
49            redaction::SyncRoomRedactionEvent,
50            tombstone::PossiblyRedactedRoomTombstoneEventContent,
51            topic::PossiblyRedactedRoomTopicEventContent,
52        },
53        rtc::notification::CallIntent,
54        tag::{TagEventContent, TagName, Tags},
55    },
56    room::RoomType,
57    room_version_rules::{RedactionRules, RoomVersionRules},
58    serde::Raw,
59};
60use serde::{Deserialize, Serialize};
61use tokio::sync::MutexGuard;
62use tracing::{field::debug, info, instrument, warn};
63
64use super::{
65    AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
66    RoomHero, RoomNotableTags, RoomState, RoomSummary,
67};
68use crate::{
69    MinimalStateEvent, StateChanges, StoreError,
70    deserialized_responses::RawSyncOrStrippedState,
71    latest_event::LatestEventValue,
72    notification_settings::RoomNotificationMode,
73    read_receipts::RoomReadReceipts,
74    room::call::CallIntentConsensus,
75    store::{IncorrectMutexGuardError, SaveLockedStateStore, StateStoreExt},
76    sync::UnreadNotificationsCount,
77    utils::{AnyStateEventEnum, RawStateEventWithKeys},
78};
79
80/// The default value of the maximum power level.
81const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
82
83impl Room {
84    /// Subscribe to the inner `RoomInfo`.
85    pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
86        self.info.subscribe()
87    }
88
89    /// Clone the inner `RoomInfo`.
90    pub fn clone_info(&self) -> RoomInfo {
91        self.info.get()
92    }
93
94    /// Update [`RoomInfo`] with the given function `F`. Updates are atomic as
95    /// this function acquires the lock of the underlying store before updating
96    /// the [`RoomInfo`].
97    pub async fn update_room_info<F>(&self, f: F)
98    where
99        F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
100    {
101        self.update_room_info_with_store_guard(&self.store.lock().lock().await, f)
102            .expect("should have correct mutex!")
103    }
104
105    /// Same as [`Self::update_room_info`], but allows the caller to provide a
106    /// guard for the lock of the underlying store in case it has already been
107    /// acquired.
108    ///
109    /// This function returns an [`IncorrectMutexGuardError`] if the provided
110    /// guard is not associated with the lock of the underlying store.
111    pub fn update_room_info_with_store_guard<F>(
112        &self,
113        guard: &MutexGuard<'_, ()>,
114        f: F,
115    ) -> Result<(), IncorrectMutexGuardError>
116    where
117        F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
118    {
119        if !std::ptr::eq(MutexGuard::mutex(guard), self.store.lock()) {
120            return Err(IncorrectMutexGuardError);
121        }
122
123        let (info, mut reasons) = f(self.clone_info());
124        self.info.set(info);
125
126        if reasons.is_empty() {
127            // TODO: remove this block!
128            // Read `RoomInfoNotableUpdateReasons::NONE` to understand why it must be
129            // removed.
130            reasons = RoomInfoNotableUpdateReasons::NONE;
131        }
132        let _ = self
133            .room_info_notable_update_sender
134            .send(RoomInfoNotableUpdate { room_id: self.room_id.clone(), reasons });
135
136        Ok(())
137    }
138
139    /// Same as [`Self::update_room_info`] but also saves the changes to the
140    /// underlying store.
141    pub async fn update_and_save_room_info<F>(&self, f: F) -> Result<(), StoreError>
142    where
143        F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
144    {
145        self.update_and_save_room_info_with_store_guard(&self.store.lock().lock().await, f).await
146    }
147
148    /// Same as [`Self::update_and_save_room_info`], but allows the caller to
149    /// provide a guard for the lock of the underlying store in case it has
150    /// already been acquired.
151    ///
152    /// This function returns an [`IncorrectMutexGuardError`] if the provided
153    /// guard is not associated with the lock of the underlying store.
154    pub async fn update_and_save_room_info_with_store_guard<F>(
155        &self,
156        guard: &MutexGuard<'_, ()>,
157        f: F,
158    ) -> Result<(), StoreError>
159    where
160        F: FnOnce(RoomInfo) -> (RoomInfo, RoomInfoNotableUpdateReasons),
161    {
162        let (info, reasons) = f(self.clone_info());
163        let mut changes = StateChanges::default();
164        changes.add_room(info.clone());
165        self.store.save_changes_with_guard(guard, &changes).await?;
166        self.update_room_info_with_store_guard(guard, |_| (info, reasons))?;
167        Ok(())
168    }
169}
170
171/// A base room info struct that is the backbone of normal as well as stripped
172/// rooms. Holds all the state events that are important to present a room to
173/// users.
174#[derive(Clone, Debug, Serialize, Deserialize)]
175pub struct BaseRoomInfo {
176    /// The avatar URL of this room.
177    pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
178    /// The canonical alias of this room.
179    pub(crate) canonical_alias:
180        Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
181    /// The `m.room.create` event content of this room.
182    pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
183    /// A list of user ids this room is considered as direct message, if this
184    /// room is a DM.
185    pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
186    /// The `m.room.encryption` event content that enabled E2EE in this room.
187    pub(crate) encryption: Option<PossiblyRedactedRoomEncryptionEventContent>,
188    /// The guest access policy of this room.
189    pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
190    /// The history visibility policy of this room.
191    pub(crate) history_visibility:
192        Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
193    /// The join rule policy of this room.
194    pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
195    /// The maximal power level that can be found in this room.
196    pub(crate) max_power_level: i64,
197    /// The member hints for the room as per MSC4171, including service members,
198    /// if available.
199    pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
200    /// The `m.room.name` of this room.
201    pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
202    /// The `m.room.tombstone` event content of this room.
203    pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
204    /// The topic of this room.
205    pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
206    /// All minimal state events that containing one or more running matrixRTC
207    /// memberships.
208    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
209    pub(crate) rtc_member_events:
210        BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
211    /// Whether this room has been manually marked as unread.
212    #[serde(default)]
213    pub(crate) is_marked_unread: bool,
214    /// The source of is_marked_unread.
215    #[serde(default)]
216    pub(crate) is_marked_unread_source: AccountDataSource,
217    /// Some notable tags.
218    ///
219    /// We are not interested by all the tags. Some tags are more important than
220    /// others, and this field collects them.
221    #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
222    pub(crate) notable_tags: RoomNotableTags,
223    /// The event ID of the user's `m.fully_read` marker for this room, if any.
224    #[serde(skip_serializing_if = "Option::is_none", default)]
225    pub(crate) fully_read_event_id: Option<OwnedEventId>,
226    /// The `m.room.pinned_events` of this room.
227    pub(crate) pinned_events: Option<PossiblyRedactedRoomPinnedEventsEventContent>,
228}
229
230impl BaseRoomInfo {
231    /// Create a new, empty base room info.
232    pub fn new() -> Self {
233        Self::default()
234    }
235
236    /// Get the room version of this room.
237    ///
238    /// For room versions earlier than room version 11, if the event is
239    /// redacted, this will return the default of [`RoomVersionId::V1`].
240    pub fn room_version(&self) -> Option<&RoomVersionId> {
241        Some(&self.create.as_ref()?.content.room_version)
242    }
243
244    /// Handle a state event for this room and update our info accordingly.
245    ///
246    /// Returns true if the event modified the info, false otherwise.
247    pub fn handle_state_event<T: AnyStateEventEnum>(
248        &mut self,
249        raw_event: &mut RawStateEventWithKeys<T>,
250    ) -> bool {
251        match (&raw_event.event_type, raw_event.state_key.as_str()) {
252            (StateEventType::RoomEncryption, "") => {
253                // To avoid breaking encrypted rooms, we ignore `m.room.encryption` events that
254                // fail to deserialize or that are redacted (i.e. they don't contain the
255                // algorithm used for encryption).
256                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
257                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomEncryption)
258                }) && event.content.algorithm.is_some()
259                {
260                    self.encryption = Some(event.content);
261                    true
262                } else {
263                    false
264                }
265            }
266            (StateEventType::RoomAvatar, "") => {
267                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
268                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomAvatar)
269                }) {
270                    self.avatar = Some(event);
271                    true
272                } else {
273                    // Remove the previous content if the new content is unknown.
274                    self.avatar.take().is_some()
275                }
276            }
277            (StateEventType::RoomName, "") => {
278                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
279                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomName)
280                }) {
281                    self.name = Some(event);
282                    true
283                } else {
284                    // Remove the previous content if the new content is unknown.
285                    self.name.take().is_some()
286                }
287            }
288            // `m.room.create` CANNOT be overwritten.
289            (StateEventType::RoomCreate, "") if self.create.is_none() => {
290                if let Some(any_event) = raw_event.deserialize()
291                    && let Some(content) = as_variant!(
292                        any_event.get_content(),
293                        AnyPossiblyRedactedStateEventContent::RoomCreate
294                    )
295                {
296                    self.create = Some(MinimalStateEvent {
297                        content: RoomCreateWithCreatorEventContent::from_event_content(
298                            content,
299                            any_event.get_sender().to_owned(),
300                        ),
301                        event_id: any_event.get_event_id().map(ToOwned::to_owned),
302                    });
303                    true
304                } else {
305                    false
306                }
307            }
308            (StateEventType::RoomHistoryVisibility, "") => {
309                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
310                    as_variant!(
311                        any_event,
312                        AnyPossiblyRedactedStateEventContent::RoomHistoryVisibility
313                    )
314                }) {
315                    self.history_visibility = Some(event);
316                    true
317                } else {
318                    // Remove the previous content if the new content is unknown.
319                    self.history_visibility.take().is_some()
320                }
321            }
322            (StateEventType::RoomGuestAccess, "") => {
323                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
324                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomGuestAccess)
325                }) {
326                    self.guest_access = Some(event);
327                    true
328                } else {
329                    // Remove the previous content if the new content is unknown.
330                    self.guest_access.take().is_some()
331                }
332            }
333            (StateEventType::MemberHints, "") => {
334                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
335                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::MemberHints)
336                }) {
337                    self.member_hints = Some(event);
338                    true
339                } else {
340                    // Remove the previous content if the new content is unknown.
341                    self.member_hints.take().is_some()
342                }
343            }
344            (StateEventType::RoomJoinRules, "") => {
345                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
346                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomJoinRules)
347                }) {
348                    match &event.content.join_rule {
349                        JoinRule::Invite
350                        | JoinRule::Knock
351                        | JoinRule::Private
352                        | JoinRule::Restricted(_)
353                        | JoinRule::KnockRestricted(_)
354                        | JoinRule::Public => {
355                            self.join_rules = Some(event);
356                            true
357                        }
358                        r => {
359                            warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
360                            // Remove the previous content if the new content is unsupported.
361                            self.join_rules.take().is_some()
362                        }
363                    }
364                } else {
365                    // Remove the previous content if the new content is unknown.
366                    self.join_rules.take().is_some()
367                }
368            }
369            (StateEventType::RoomCanonicalAlias, "") => {
370                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
371                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomCanonicalAlias)
372                }) {
373                    self.canonical_alias = Some(event);
374                    true
375                } else {
376                    // Remove the previous content if the new content is unknown.
377                    self.canonical_alias.take().is_some()
378                }
379            }
380            (StateEventType::RoomTopic, "") => {
381                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
382                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTopic)
383                }) {
384                    self.topic = Some(event);
385                    true
386                } else {
387                    // Remove the previous content if the new content is unknown.
388                    self.topic.take().is_some()
389                }
390            }
391            (StateEventType::RoomTombstone, "") => {
392                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
393                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomTombstone)
394                }) {
395                    self.tombstone = Some(event);
396                    true
397                } else {
398                    // Remove the previous content if the new content is unknown.
399                    self.tombstone.take().is_some()
400                }
401            }
402            (StateEventType::RoomPowerLevels, "") => {
403                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
404                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPowerLevels)
405                }) {
406                    let new_max = i64::from(
407                        event
408                            .content
409                            .users
410                            .values()
411                            .fold(event.content.users_default, |max_pl, user_pl| {
412                                max_pl.max(*user_pl)
413                            }),
414                    );
415
416                    if self.max_power_level != new_max {
417                        self.max_power_level = new_max;
418                        true
419                    } else {
420                        false
421                    }
422                } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
423                    // Reset the previous value if the new value is unknown.
424                    self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
425                    true
426                } else {
427                    false
428                }
429            }
430            (StateEventType::CallMember, _) => {
431                if let Ok(call_member_key) = raw_event.state_key.parse::<CallMemberStateKey>() {
432                    if let Some(any_event) = raw_event.deserialize()
433                        && let Some(content) = as_variant!(
434                            any_event.get_content(),
435                            AnyPossiblyRedactedStateEventContent::CallMember
436                        )
437                    {
438                        let mut event = MinimalStateEvent {
439                            content,
440                            event_id: any_event.get_event_id().map(ToOwned::to_owned),
441                        };
442
443                        if let Some(origin_server_ts) = any_event.get_origin_server_ts() {
444                            event.content.set_created_ts_if_none(origin_server_ts);
445                        }
446
447                        // Add the new event.
448                        self.rtc_member_events.insert(call_member_key, event);
449
450                        // Remove all events that don't contain any memberships anymore.
451                        self.rtc_member_events
452                            .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
453
454                        true
455                    } else {
456                        // Remove the previous content with the same state key if the new content is
457                        // unknown.
458                        self.rtc_member_events.remove(&call_member_key).is_some()
459                    }
460                } else {
461                    false
462                }
463            }
464            (StateEventType::RoomPinnedEvents, "") => {
465                if let Some(event) = raw_event.deserialize_as_minimal_event(|any_event| {
466                    as_variant!(any_event, AnyPossiblyRedactedStateEventContent::RoomPinnedEvents)
467                }) {
468                    self.pinned_events = Some(event.content);
469                    true
470                } else {
471                    // Remove the previous content if the new content is unknown.
472                    self.pinned_events.take().is_some()
473                }
474            }
475            _ => false,
476        }
477    }
478
479    pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
480        let redaction_rules = self
481            .room_version()
482            .and_then(|room_version| room_version.rules())
483            .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
484            .redaction;
485
486        if let Some(ev) = &mut self.avatar
487            && ev.event_id.as_deref() == Some(redacts)
488        {
489            ev.redact(&redaction_rules);
490        } else if let Some(ev) = &mut self.canonical_alias
491            && ev.event_id.as_deref() == Some(redacts)
492        {
493            ev.redact(&redaction_rules);
494        } else if let Some(ev) = &mut self.create
495            && ev.event_id.as_deref() == Some(redacts)
496        {
497            ev.redact(&redaction_rules);
498        } else if let Some(ev) = &mut self.guest_access
499            && ev.event_id.as_deref() == Some(redacts)
500        {
501            ev.redact(&redaction_rules);
502        } else if let Some(ev) = &mut self.history_visibility
503            && ev.event_id.as_deref() == Some(redacts)
504        {
505            ev.redact(&redaction_rules);
506        } else if let Some(ev) = &mut self.join_rules
507            && ev.event_id.as_deref() == Some(redacts)
508        {
509            ev.redact(&redaction_rules);
510        } else if let Some(ev) = &mut self.name
511            && ev.event_id.as_deref() == Some(redacts)
512        {
513            ev.redact(&redaction_rules);
514        } else if let Some(ev) = &mut self.tombstone
515            && ev.event_id.as_deref() == Some(redacts)
516        {
517            ev.redact(&redaction_rules);
518        } else if let Some(ev) = &mut self.topic
519            && ev.event_id.as_deref() == Some(redacts)
520        {
521            ev.redact(&redaction_rules);
522        } else {
523            self.rtc_member_events
524                .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
525        }
526    }
527
528    pub fn handle_notable_tags(&mut self, tags: &Tags) {
529        let mut notable_tags = RoomNotableTags::empty();
530
531        if tags.contains_key(&TagName::Favorite) {
532            notable_tags.insert(RoomNotableTags::FAVOURITE);
533        }
534
535        if tags.contains_key(&TagName::LowPriority) {
536            notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
537        }
538
539        self.notable_tags = notable_tags;
540    }
541}
542
543impl Default for BaseRoomInfo {
544    fn default() -> Self {
545        Self {
546            avatar: None,
547            canonical_alias: None,
548            create: None,
549            dm_targets: Default::default(),
550            member_hints: None,
551            encryption: None,
552            guest_access: None,
553            history_visibility: None,
554            join_rules: None,
555            max_power_level: DEFAULT_MAX_POWER_LEVEL,
556            name: None,
557            tombstone: None,
558            topic: None,
559            rtc_member_events: BTreeMap::new(),
560            is_marked_unread: false,
561            is_marked_unread_source: AccountDataSource::Unstable,
562            notable_tags: RoomNotableTags::empty(),
563            fully_read_event_id: None,
564            pinned_events: None,
565        }
566    }
567}
568
569/// The underlying pure data structure for joined and left rooms.
570///
571/// Holds all the info needed to persist a room into the state store.
572#[derive(Clone, Debug, Serialize, Deserialize)]
573pub struct RoomInfo {
574    /// The version of the room info type. It is used to migrate the `RoomInfo`
575    /// serialization from one version to another.
576    #[serde(default, alias = "version")]
577    pub(crate) data_format_version: u8,
578
579    /// The unique room id of the room.
580    pub(crate) room_id: OwnedRoomId,
581
582    /// The state of the room.
583    pub(crate) room_state: RoomState,
584
585    /// The unread notifications counts, as returned by the server.
586    ///
587    /// These might be incorrect for encrypted rooms, since the server doesn't
588    /// have access to the content of the encrypted events.
589    pub(crate) notification_counts: UnreadNotificationsCount,
590
591    /// The summary of this room.
592    pub(crate) summary: RoomSummary,
593
594    /// Flag remembering if the room members are synced.
595    pub(crate) members_synced: bool,
596
597    /// The prev batch of this room we received during the last sync.
598    pub(crate) last_prev_batch: Option<String>,
599
600    /// How much we know about this room.
601    pub(crate) sync_info: SyncInfo,
602
603    /// Whether or not the encryption info was been synced.
604    pub(crate) encryption_state_synced: bool,
605
606    /// The latest event value of this room.
607    #[serde(default)]
608    pub(crate) latest_event_value: LatestEventValue,
609
610    /// Information about read receipts for this room.
611    #[serde(default)]
612    pub(crate) read_receipts: RoomReadReceipts,
613
614    /// Base room info which holds some basic event contents important for the
615    /// room state.
616    pub(crate) base_info: Box<BaseRoomInfo>,
617
618    /// Whether we already warned about unknown room version rules in
619    /// [`RoomInfo::room_version_rules_or_default`]. This is done to avoid
620    /// spamming about unknown room versions rules in the log for the same room.
621    #[serde(skip)]
622    pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
623
624    /// Cached display name, useful for sync access.
625    ///
626    /// Filled by calling [`Room::compute_display_name`]. It's automatically
627    /// filled at start when creating a room, or on every successful sync.
628    #[serde(default, skip_serializing_if = "Option::is_none")]
629    pub(crate) cached_display_name: Option<RoomDisplayName>,
630
631    /// Cached user defined notification mode.
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
634
635    /// The recency stamp of this room.
636    ///
637    /// It's not to be confused with the `origin_server_ts` value of an event.
638    /// Sliding Sync might “ignore” some events when computing the recency
639    /// stamp of the room. The recency stamp must be considered as an opaque
640    /// unsigned integer value.
641    ///
642    /// # Sorting rooms
643    ///
644    /// The recency stamp is designed to _sort_ rooms between them. The room
645    /// with the highest stamp should be at the top of a room list. However, in
646    /// some situation, it might be inaccurate (for example if the server and
647    /// the client disagree on which events should increment the recency stamp).
648    /// The [`LatestEventValue`] might be a useful alternative to sort rooms
649    /// between them as it's all computed client-side. In this case, the recency
650    /// stamp nicely acts as a default fallback.
651    #[serde(default)]
652    pub(crate) recency_stamp: Option<RoomRecencyStamp>,
653}
654
655impl RoomInfo {
656    #[doc(hidden)] // used by store tests, otherwise it would be pub(crate)
657    pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
658        Self {
659            data_format_version: 1,
660            room_id: room_id.into(),
661            room_state,
662            notification_counts: Default::default(),
663            summary: Default::default(),
664            members_synced: false,
665            last_prev_batch: None,
666            sync_info: SyncInfo::NoState,
667            encryption_state_synced: false,
668            latest_event_value: LatestEventValue::default(),
669            read_receipts: Default::default(),
670            base_info: Box::new(BaseRoomInfo::new()),
671            warned_about_unknown_room_version_rules: Arc::new(false.into()),
672            cached_display_name: None,
673            cached_user_defined_notification_mode: None,
674            recency_stamp: None,
675        }
676    }
677
678    /// Mark this Room as joined.
679    pub fn mark_as_joined(&mut self) {
680        self.set_state(RoomState::Joined);
681    }
682
683    /// Mark this Room as left.
684    pub fn mark_as_left(&mut self) {
685        self.set_state(RoomState::Left);
686    }
687
688    /// Mark this Room as invited.
689    pub fn mark_as_invited(&mut self) {
690        self.set_state(RoomState::Invited);
691    }
692
693    /// Mark this Room as knocked.
694    pub fn mark_as_knocked(&mut self) {
695        self.set_state(RoomState::Knocked);
696    }
697
698    /// Mark this Room as banned.
699    pub fn mark_as_banned(&mut self) {
700        self.set_state(RoomState::Banned);
701    }
702
703    /// Set the membership RoomState of this Room
704    pub fn set_state(&mut self, room_state: RoomState) {
705        self.room_state = room_state;
706    }
707
708    /// Mark this Room as having all the members synced.
709    pub fn mark_members_synced(&mut self) {
710        self.members_synced = true;
711    }
712
713    /// Mark this Room as still missing member information.
714    pub fn mark_members_missing(&mut self) {
715        self.members_synced = false;
716    }
717
718    /// Returns whether the room members are synced.
719    pub fn are_members_synced(&self) -> bool {
720        self.members_synced
721    }
722
723    /// Mark this Room as still missing some state information.
724    pub fn mark_state_partially_synced(&mut self) {
725        self.sync_info = SyncInfo::PartiallySynced;
726    }
727
728    /// Mark this Room as still having all state synced.
729    pub fn mark_state_fully_synced(&mut self) {
730        self.sync_info = SyncInfo::FullySynced;
731    }
732
733    /// Mark this Room as still having no state synced.
734    pub fn mark_state_not_synced(&mut self) {
735        self.sync_info = SyncInfo::NoState;
736    }
737
738    /// Mark this Room as having the encryption state synced.
739    pub fn mark_encryption_state_synced(&mut self) {
740        self.encryption_state_synced = true;
741    }
742
743    /// Mark this Room as still missing encryption state information.
744    pub fn mark_encryption_state_missing(&mut self) {
745        self.encryption_state_synced = false;
746    }
747
748    /// Set the `prev_batch`-token.
749    /// Returns whether the token has differed and thus has been upgraded:
750    /// `false` means no update was applied as the were the same
751    pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
752        if self.last_prev_batch.as_deref() != prev_batch {
753            self.last_prev_batch = prev_batch.map(|p| p.to_owned());
754            true
755        } else {
756            false
757        }
758    }
759
760    /// Returns the state this room is in.
761    pub fn state(&self) -> RoomState {
762        self.room_state
763    }
764
765    /// Returns the encryption state of this room.
766    #[cfg(not(feature = "experimental-encrypted-state-events"))]
767    pub fn encryption_state(&self) -> EncryptionState {
768        if !self.encryption_state_synced {
769            EncryptionState::Unknown
770        } else if self.base_info.encryption.is_some() {
771            EncryptionState::Encrypted
772        } else {
773            EncryptionState::NotEncrypted
774        }
775    }
776
777    /// Returns the encryption state of this room.
778    #[cfg(feature = "experimental-encrypted-state-events")]
779    pub fn encryption_state(&self) -> EncryptionState {
780        if !self.encryption_state_synced {
781            EncryptionState::Unknown
782        } else {
783            self.base_info
784                .encryption
785                .as_ref()
786                .map(|state| {
787                    if state.encrypt_state_events {
788                        EncryptionState::StateEncrypted
789                    } else {
790                        EncryptionState::Encrypted
791                    }
792                })
793                .unwrap_or(EncryptionState::NotEncrypted)
794        }
795    }
796
797    /// Set the encryption event content in this room.
798    pub fn set_encryption_event(
799        &mut self,
800        event: Option<PossiblyRedactedRoomEncryptionEventContent>,
801    ) {
802        self.base_info.encryption = event;
803    }
804
805    /// Handle the encryption state.
806    pub fn handle_encryption_state(
807        &mut self,
808        requested_required_states: &[(StateEventType, String)],
809    ) {
810        if requested_required_states
811            .iter()
812            .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
813        {
814            // The `m.room.encryption` event was requested during the sync. Whether we have
815            // received a `m.room.encryption` event in return doesn't matter: we must mark
816            // the encryption state as synced; if the event is present, it means the room
817            // _is_ encrypted, otherwise it means the room _is not_ encrypted.
818
819            self.mark_encryption_state_synced();
820        }
821    }
822
823    /// Handle the given state event.
824    ///
825    /// Returns true if the event modified the info, false otherwise.
826    pub fn handle_state_event(
827        &mut self,
828        raw_event: &mut RawStateEventWithKeys<AnySyncStateEvent>,
829    ) -> bool {
830        // When we receive a `m.room.member_hints` event
831        if raw_event.event_type == StateEventType::MemberHints
832            && let Some(AnySyncStateEvent::MemberHints(new_hints)) = raw_event.deserialize()
833            // If we have both old and new member hints events
834            && let (Some(current_hints), Some(new)) =
835                (&self.base_info.member_hints, new_hints.as_original())
836            // Then we check if their contents don't match
837            && current_hints
838                .content
839                .service_members
840                .as_ref()
841                .is_some_and(|current_members| *current_members != new.content.service_members)
842        {
843            // And reset the computed value in that case
844            self.summary.active_service_members = None;
845        }
846
847        // Store the state event in the `BaseRoomInfo`.
848        let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
849
850        if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
851        {
852            // The `m.room.encryption` event was or wasn't explicitly requested, we don't
853            // know here (see `Self::handle_encryption_state`) but we got one in
854            // return! In this case, we can deduce the room _is_ encrypted, but we cannot
855            // know if it _is not_ encrypted.
856
857            self.mark_encryption_state_synced();
858        }
859
860        base_info_has_been_modified
861    }
862
863    /// Handle the given stripped state event.
864    ///
865    /// Returns true if the event modified the info, false otherwise.
866    pub fn handle_stripped_state_event(
867        &mut self,
868        raw_event: &mut RawStateEventWithKeys<AnyStrippedStateEvent>,
869    ) -> bool {
870        self.base_info.handle_state_event(raw_event)
871    }
872
873    /// Handle the given redaction.
874    #[instrument(skip_all, fields(redacts))]
875    pub fn handle_redaction(
876        &mut self,
877        event: &SyncRoomRedactionEvent,
878        _raw: &Raw<SyncRoomRedactionEvent>,
879    ) {
880        let redaction_rules = self.room_version_rules_or_default().redaction;
881
882        let Some(redacts) = event.redacts(&redaction_rules) else {
883            info!("Can't apply redaction, redacts field is missing");
884            return;
885        };
886        tracing::Span::current().record("redacts", debug(redacts));
887
888        self.base_info.handle_redaction(redacts);
889    }
890
891    /// Returns the current room avatar.
892    pub fn avatar_url(&self) -> Option<&MxcUri> {
893        self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
894    }
895
896    /// Update the room avatar.
897    pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
898        self.base_info.avatar = url.map(|url| {
899            let mut content = PossiblyRedactedRoomAvatarEventContent::new();
900            content.url = Some(url);
901
902            MinimalStateEvent { content, event_id: None }
903        });
904    }
905
906    /// Returns information about the current room avatar.
907    pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
908        self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
909    }
910
911    /// Update the notifications count.
912    pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
913        self.notification_counts = notification_counts;
914    }
915
916    /// Update the RoomSummary from a Ruma `RoomSummary`.
917    ///
918    /// Returns true if any field has been updated, false otherwise.
919    pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
920        let mut changed = false;
921
922        if !summary.is_empty() {
923            if !summary.heroes.is_empty() {
924                self.summary.room_heroes = summary
925                    .heroes
926                    .iter()
927                    .map(|hero_id| RoomHero {
928                        user_id: hero_id.to_owned(),
929                        display_name: None,
930                        avatar_url: None,
931                    })
932                    .collect();
933
934                changed = true;
935            }
936
937            if let Some(joined) = summary.joined_member_count {
938                self.summary.joined_member_count = joined.into();
939                changed = true;
940            }
941
942            if let Some(invited) = summary.invited_member_count {
943                self.summary.invited_member_count = invited.into();
944                changed = true;
945            }
946        }
947
948        if changed {
949            self.summary.active_service_members = None;
950        }
951
952        changed
953    }
954
955    /// Updates the joined member count.
956    pub(crate) fn update_joined_member_count(&mut self, count: u64) {
957        self.summary.joined_member_count = count;
958    }
959
960    /// Updates the invited member count.
961    pub(crate) fn update_invited_member_count(&mut self, count: u64) {
962        self.summary.invited_member_count = count;
963    }
964
965    /// Updates the room heroes.
966    pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
967        self.summary.room_heroes = heroes;
968    }
969
970    /// The heroes for this room.
971    pub fn heroes(&self) -> &[RoomHero] {
972        &self.summary.room_heroes
973    }
974
975    /// The number of active members (invited + joined) in the room.
976    ///
977    /// The return value is saturated at `u64::MAX`.
978    pub fn active_members_count(&self) -> u64 {
979        self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
980    }
981
982    /// The number of invited members in the room
983    pub fn invited_members_count(&self) -> u64 {
984        self.summary.invited_member_count
985    }
986
987    /// The number of joined members in the room
988    pub fn joined_members_count(&self) -> u64 {
989        self.summary.joined_member_count
990    }
991
992    /// Get the canonical alias of this room.
993    pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
994        self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
995    }
996
997    /// Get the alternative aliases of this room.
998    pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
999        self.base_info
1000            .canonical_alias
1001            .as_ref()
1002            .map(|ev| ev.content.alt_aliases.as_ref())
1003            .unwrap_or_default()
1004    }
1005
1006    /// Get the room ID of this room.
1007    pub fn room_id(&self) -> &RoomId {
1008        &self.room_id
1009    }
1010
1011    /// Get the room version of this room.
1012    pub fn room_version(&self) -> Option<&RoomVersionId> {
1013        self.base_info.room_version()
1014    }
1015
1016    /// Get the room version rules of this room, or a sensible default.
1017    ///
1018    /// Will warn (at most once) if the room create event is missing from this
1019    /// [`RoomInfo`] or if the room version is unsupported.
1020    pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
1021        use std::sync::atomic::Ordering;
1022
1023        self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
1024            || {
1025                if self
1026                    .warned_about_unknown_room_version_rules
1027                    .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
1028                    .is_ok()
1029                {
1030                    warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
1031                }
1032
1033                ROOM_VERSION_RULES_FALLBACK
1034            },
1035        )
1036    }
1037
1038    /// Get the room type of this room.
1039    pub fn room_type(&self) -> Option<&RoomType> {
1040        self.base_info.create.as_ref()?.content.room_type.as_ref()
1041    }
1042
1043    /// Get the creators of this room.
1044    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
1045        Some(self.base_info.create.as_ref()?.content.creators())
1046    }
1047
1048    pub(super) fn guest_access(&self) -> &GuestAccess {
1049        self.base_info
1050            .guest_access
1051            .as_ref()
1052            .and_then(|event| event.content.guest_access.as_ref())
1053            .unwrap_or(&GuestAccess::Forbidden)
1054    }
1055
1056    /// Returns the history visibility for this room.
1057    ///
1058    /// Returns None if the event was never seen during sync.
1059    pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1060        Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1061    }
1062
1063    /// Returns the history visibility for this room, or a sensible default.
1064    ///
1065    /// Returns `Shared`, the default specified by the [spec], when the event is
1066    /// missing.
1067    ///
1068    /// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7
1069    pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1070        self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1071    }
1072
1073    /// Return the join rule for this room, if the `m.room.join_rules` event is
1074    /// available.
1075    pub fn join_rule(&self) -> Option<&JoinRule> {
1076        Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1077    }
1078
1079    /// Return the service members for this room if the `m.member_hints` event
1080    /// is available
1081    pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1082        self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1083    }
1084
1085    /// Get the name of this room.
1086    pub fn name(&self) -> Option<&str> {
1087        self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1088    }
1089
1090    /// Get the content of the `m.room.create` event if any.
1091    pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1092        Some(&self.base_info.create.as_ref()?.content)
1093    }
1094
1095    /// Get the content of the `m.room.tombstone` event if any.
1096    pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1097        Some(&self.base_info.tombstone.as_ref()?.content)
1098    }
1099
1100    /// Returns the topic for this room, if set.
1101    pub fn topic(&self) -> Option<&str> {
1102        self.base_info.topic.as_ref()?.content.topic.as_deref()
1103    }
1104
1105    /// Get a list of all the valid (non expired) matrixRTC memberships and
1106    /// associated UserId's in this room.
1107    ///
1108    /// The vector is ordered by oldest membership to newest.
1109    fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1110        let mut v = self
1111            .base_info
1112            .rtc_member_events
1113            .iter()
1114            .flat_map(|(state_key, ev)| {
1115                ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1116            })
1117            .collect::<Vec<_>>();
1118        v.sort_by_key(|(_, m)| m.created_ts());
1119        v
1120    }
1121
1122    /// Similar to
1123    /// [`matrix_rtc_memberships`](Self::active_matrix_rtc_memberships) but only
1124    /// returns Memberships with application "m.call" and scope "m.room".
1125    ///
1126    /// The vector is ordered by oldest membership user to newest.
1127    fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1128        self.active_matrix_rtc_memberships()
1129            .into_iter()
1130            .filter(|(_user_id, m)| m.is_room_call())
1131            .collect()
1132    }
1133
1134    /// Is there a non expired membership with application "m.call" and scope
1135    /// "m.room" in this room.
1136    pub fn has_active_room_call(&self) -> bool {
1137        !self.active_room_call_memberships().is_empty()
1138    }
1139
1140    /// Get the call intent consensus for the current call, based on what
1141    /// members are advertising.
1142    ///
1143    /// This provides detailed information about the consensus state (is it an
1144    /// audio or video call), including whether it's full (all members
1145    /// agree) or partial (only some members advertise), allowing callers to
1146    /// distinguish between different levels of consensus.
1147    ///
1148    /// # Returns
1149    ///
1150    /// - [`CallIntentConsensus::Full`] if all members advertise and agree on
1151    ///   the same intent
1152    /// - [`CallIntentConsensus::Partial`] if only some members advertise but
1153    ///   those who do agree
1154    /// - [`CallIntentConsensus::None`] if no one advertises or advertisers
1155    ///   disagree
1156    pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1157        let memberships = self.active_room_call_memberships();
1158        let total_count: u64 = memberships.len() as u64;
1159
1160        if total_count == 0 {
1161            return CallIntentConsensus::None;
1162        }
1163
1164        // Track the first intent found and count how many members advertise it
1165        let mut consensus_intent: Option<CallIntent> = None;
1166        let mut agreeing_count: u64 = 0;
1167
1168        for (_, data) in memberships.iter() {
1169            if let Some(intent) = data.call_intent() {
1170                match &consensus_intent {
1171                    // First intent found, set it as consensus
1172                    None => {
1173                        consensus_intent = Some(intent.clone());
1174                        agreeing_count = 1;
1175                    }
1176                    // Check if this intent matches the consensus
1177                    Some(current) if current == intent => {
1178                        agreeing_count += 1;
1179                    }
1180                    // Intents differ, no consensus
1181                    Some(_) => return CallIntentConsensus::None,
1182                }
1183            }
1184        }
1185
1186        // Return the appropriate consensus type based on participation
1187        match consensus_intent {
1188            None => CallIntentConsensus::None,
1189            Some(intent) if agreeing_count == total_count => {
1190                // All members advertise and agree
1191                CallIntentConsensus::Full(intent)
1192            }
1193            Some(intent) => {
1194                // Some members advertise and agree, others don't advertise
1195                CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1196            }
1197        }
1198    }
1199
1200    /// Returns a Vec of userId's that participate in the room call.
1201    ///
1202    /// matrix_rtc memberships with application "m.call" and scope "m.room" are
1203    /// considered. A user can occur twice if they join with two devices.
1204    /// convert to a set depending if the different users are required or the
1205    /// amount of sessions.
1206    ///
1207    /// The vector is ordered by oldest membership user to newest.
1208    pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1209        self.active_room_call_memberships()
1210            .iter()
1211            .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1212            .collect()
1213    }
1214
1215    /// Sets the new [`LatestEventValue`].
1216    pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1217        self.latest_event_value = new_value;
1218    }
1219
1220    /// Updates the recency stamp of this room.
1221    ///
1222    /// Please read `Self::recency_stamp` to learn more.
1223    pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1224        self.recency_stamp = Some(stamp);
1225    }
1226
1227    /// Returns the current pinned event ids for this room.
1228    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1229        self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1230    }
1231
1232    /// Returns the event ID of the user's `m.fully_read` marker for this room,
1233    /// if any.
1234    pub fn fully_read_event_id(&self) -> Option<&EventId> {
1235        self.base_info.fully_read_event_id.as_deref()
1236    }
1237
1238    /// Checks if an `EventId` is currently pinned.
1239    /// It avoids having to clone the whole list of event ids to check a single
1240    /// value.
1241    ///
1242    /// Returns `true` if the provided `event_id` is pinned, `false` otherwise.
1243    pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1244        self.base_info
1245            .pinned_events
1246            .as_ref()
1247            .and_then(|content| content.pinned.as_deref())
1248            .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1249    }
1250
1251    /// Returns the computed read receipts for this room.
1252    pub fn read_receipts(&self) -> &RoomReadReceipts {
1253        &self.read_receipts
1254    }
1255
1256    /// Set the computed read receipts for this room.
1257    pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1258        self.read_receipts = read_receipts;
1259    }
1260
1261    /// Apply migrations to this `RoomInfo` if needed.
1262    ///
1263    /// This should be used to populate new fields with data from the state
1264    /// store.
1265    ///
1266    /// Returns `true` if migrations were applied and this `RoomInfo` needs to
1267    /// be persisted to the state store.
1268    #[instrument(skip_all, fields(room_id = ?self.room_id))]
1269    pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1270        let mut migrated = false;
1271
1272        if self.data_format_version < 1 {
1273            info!("Migrating room info to version 1");
1274
1275            // notable_tags
1276            match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1277                // Pinned events are never in stripped state.
1278                Ok(Some(raw_event)) => match raw_event.deserialize() {
1279                    Ok(event) => {
1280                        self.base_info.handle_notable_tags(&event.content.tags);
1281                    }
1282                    Err(error) => {
1283                        warn!("Failed to deserialize room tags: {error}");
1284                    }
1285                },
1286                Ok(_) => {
1287                    // Nothing to do.
1288                }
1289                Err(error) => {
1290                    warn!("Failed to load room tags: {error}");
1291                }
1292            }
1293
1294            // pinned_events
1295            match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1296            {
1297                // Pinned events are never in stripped state.
1298                Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1299                    if let Some(mut raw_event) =
1300                        RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1301                    {
1302                        self.handle_state_event(&mut raw_event);
1303                    }
1304                }
1305                Ok(_) => {
1306                    // Nothing to do.
1307                }
1308                Err(error) => {
1309                    warn!("Failed to load room pinned events: {error}");
1310                }
1311            }
1312
1313            self.data_format_version = 1;
1314            migrated = true;
1315        }
1316
1317        migrated
1318    }
1319
1320    /// Returns the number of active (joined/invited) service members in the
1321    /// room, if known.
1322    pub fn active_service_member_count(&self) -> Option<u64> {
1323        self.summary.active_service_members
1324    }
1325
1326    /// Updates the cached value for the number of active service members in the
1327    /// room.
1328    pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1329        self.summary.active_service_members = count;
1330    }
1331}
1332
1333/// Type to represent a `RoomInfo::recency_stamp`.
1334#[repr(transparent)]
1335#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1336#[serde(transparent)]
1337pub struct RoomRecencyStamp(u64);
1338
1339impl From<u64> for RoomRecencyStamp {
1340    fn from(value: u64) -> Self {
1341        Self(value)
1342    }
1343}
1344
1345impl From<RoomRecencyStamp> for u64 {
1346    fn from(value: RoomRecencyStamp) -> Self {
1347        value.0
1348    }
1349}
1350
1351#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1352pub(crate) enum SyncInfo {
1353    /// We only know the room exists and whether it is in invite / joined / left
1354    /// state.
1355    ///
1356    /// This is the case when we have a limited sync or only seen the room
1357    /// because of a request we've done, like a room creation event.
1358    NoState,
1359
1360    /// Some states have been synced, but they might have been filtered or is
1361    /// stale, as it is from a room we've left.
1362    PartiallySynced,
1363
1364    /// We have all the latest state events.
1365    FullySynced,
1366}
1367
1368/// Apply a redaction to the given target `event`, given the raw redaction event
1369/// and the room version.
1370pub fn apply_redaction(
1371    event: &Raw<AnySyncTimelineEvent>,
1372    raw_redaction: &Raw<SyncRoomRedactionEvent>,
1373    rules: &RedactionRules,
1374) -> Option<Raw<AnySyncTimelineEvent>> {
1375    use ruma::canonical_json::{RedactedBecause, redact_in_place};
1376
1377    let mut event_json = match event.deserialize_as() {
1378        Ok(json) => json,
1379        Err(e) => {
1380            warn!("Failed to deserialize latest event: {e}");
1381            return None;
1382        }
1383    };
1384
1385    let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1386        Ok(rb) => rb,
1387        Err(e) => {
1388            warn!("Redaction event is not valid canonical JSON: {e}");
1389            return None;
1390        }
1391    };
1392
1393    let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1394
1395    if let Err(e) = redact_result {
1396        warn!("Failed to redact event: {e}");
1397        return None;
1398    }
1399
1400    let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1401    Some(raw.cast_unchecked())
1402}
1403
1404/// Indicates that a notable update of `RoomInfo` has been applied, and why.
1405///
1406/// A room info notable update is an update that can be interesting for other
1407/// parts of the code. This mechanism is used in coordination with
1408/// [`BaseClient::room_info_notable_update_receiver`][baseclient] (and
1409/// `Room::info` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
1410/// can be observed and some of its updates can be spread to listeners.
1411///
1412/// [baseclient]: crate::BaseClient::room_info_notable_update_receiver
1413#[derive(Debug, Clone)]
1414pub struct RoomInfoNotableUpdate {
1415    /// The room which was updated.
1416    pub room_id: OwnedRoomId,
1417
1418    /// The reason for this update.
1419    pub reasons: RoomInfoNotableUpdateReasons,
1420}
1421
1422bitflags! {
1423    /// The reason why a [`RoomInfoNotableUpdate`] is emitted.
1424    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1425    pub struct RoomInfoNotableUpdateReasons: u16 {
1426        /// The recency stamp of the `Room` has changed.
1427        const RECENCY_STAMP = 0b0000_0000_0000_0001;
1428
1429        /// The latest event of the `Room` has changed.
1430        const LATEST_EVENT = 0b0000_0000_0000_0010;
1431
1432        /// A read receipt has changed.
1433        const READ_RECEIPT = 0b0000_0000_0000_0100;
1434
1435        /// The user-controlled unread marker value has changed.
1436        const UNREAD_MARKER = 0b0000_0000_0000_1000;
1437
1438        /// A membership change happened for the current user.
1439        const MEMBERSHIP = 0b0000_0000_0001_0000;
1440
1441        /// The display name has changed.
1442        const DISPLAY_NAME = 0b0000_0000_0010_0000;
1443
1444        /// The active service members have changed.
1445        const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;
1446
1447        /// This is a temporary hack.
1448        ///
1449        /// So here is the thing. Ideally, we DO NOT want to emit this reason. It does not
1450        /// makes sense. However, all notable update reasons are not clearly identified
1451        /// so far. Why is it a problem? The `matrix_sdk_ui::room_list_service::RoomList`
1452        /// is listening this stream of [`RoomInfoNotableUpdate`], and emits an update on a
1453        /// room item if it receives a notable reason. Because all reasons are not
1454        /// identified, we are likely to miss particular updates, and it can feel broken.
1455        /// Ultimately, we want to clearly identify all the notable update reasons, and
1456        /// remove this one.
1457        const NONE = 0b0000_0000_1000_0000;
1458
1459        /// The user's `m.fully_read` marker has changed.
1460        const FULLY_READ = 0b0000_0001_0000_0000;
1461    }
1462}
1463
1464impl Default for RoomInfoNotableUpdateReasons {
1465    fn default() -> Self {
1466        Self::empty()
1467    }
1468}
1469
1470#[cfg(test)]
1471mod tests {
1472    use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1473
1474    use assert_matches::assert_matches;
1475    use futures_util::future::{self, Either};
1476    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1477    use gloo_timers::future::sleep;
1478    use matrix_sdk_common::executor::spawn;
1479    use matrix_sdk_test::{async_test, event_factory::EventFactory};
1480    use ruma::{
1481        assign,
1482        events::{
1483            AnyRoomAccountDataEvent,
1484            room::pinned_events::RoomPinnedEventsEventContent,
1485            tag::{TagInfo, TagName, Tags, UserTagName},
1486        },
1487        owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1488        serde::Raw,
1489        user_id,
1490    };
1491    use serde_json::json;
1492    use similar_asserts::assert_eq;
1493    use tokio::sync::Mutex;
1494    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1495    use tokio::time::sleep;
1496
1497    use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1498    use crate::{
1499        RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1500        RoomState, StateChanges, StateStore,
1501        notification_settings::RoomNotificationMode,
1502        room::{RoomNotableTags, RoomSummary},
1503        store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1504        sync::UnreadNotificationsCount,
1505    };
1506
1507    #[test]
1508    fn test_room_info_serialization() {
1509        // This test exists to make sure we don't accidentally change the
1510        // serialized format for `RoomInfo`.
1511
1512        let info = RoomInfo {
1513            data_format_version: 1,
1514            room_id: room_id!("!gda78o:server.tld").into(),
1515            room_state: RoomState::Invited,
1516            notification_counts: UnreadNotificationsCount {
1517                highlight_count: 1,
1518                notification_count: 2,
1519            },
1520            summary: RoomSummary {
1521                room_heroes: vec![RoomHero {
1522                    user_id: owned_user_id!("@somebody:example.org"),
1523                    display_name: None,
1524                    avatar_url: None,
1525                }],
1526                joined_member_count: 5,
1527                invited_member_count: 0,
1528                active_service_members: None,
1529            },
1530            members_synced: true,
1531            last_prev_batch: Some("pb".to_owned()),
1532            sync_info: SyncInfo::FullySynced,
1533            encryption_state_synced: true,
1534            latest_event_value: LatestEventValue::None,
1535            base_info: Box::new(
1536                assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1537            ),
1538            read_receipts: Default::default(),
1539            warned_about_unknown_room_version_rules: Arc::new(false.into()),
1540            cached_display_name: None,
1541            cached_user_defined_notification_mode: None,
1542            recency_stamp: Some(42.into()),
1543        };
1544
1545        let info_json = json!({
1546            "data_format_version": 1,
1547            "room_id": "!gda78o:server.tld",
1548            "room_state": "Invited",
1549            "notification_counts": {
1550                "highlight_count": 1,
1551                "notification_count": 2,
1552            },
1553            "summary": {
1554                "room_heroes": [{
1555                    "user_id": "@somebody:example.org",
1556                    "display_name": null,
1557                    "avatar_url": null
1558                }],
1559                "joined_member_count": 5,
1560                "invited_member_count": 0,
1561            },
1562            "members_synced": true,
1563            "last_prev_batch": "pb",
1564            "sync_info": "FullySynced",
1565            "encryption_state_synced": true,
1566            "latest_event_value": "None",
1567            "base_info": {
1568                "avatar": null,
1569                "canonical_alias": null,
1570                "create": null,
1571                "dm_targets": [],
1572                "encryption": null,
1573                "guest_access": null,
1574                "history_visibility": null,
1575                "is_marked_unread": false,
1576                "is_marked_unread_source": "Unstable",
1577                "join_rules": null,
1578                "max_power_level": 100,
1579                "member_hints": null,
1580                "name": null,
1581                "tombstone": null,
1582                "topic": null,
1583                "pinned_events": {
1584                    "pinned": ["$a"]
1585                },
1586            },
1587            "read_receipts": {
1588                "num_unread": 0,
1589                "num_mentions": 0,
1590                "num_notifications": 0,
1591                "latest_active": null,
1592                "pending": [],
1593            },
1594            "recency_stamp": 42,
1595        });
1596
1597        assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1598    }
1599
1600    #[async_test]
1601    async fn test_room_info_migration_v1() {
1602        let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1603
1604        let room_info_json = json!({
1605            "room_id": "!gda78o:server.tld",
1606            "room_state": "Joined",
1607            "notification_counts": {
1608                "highlight_count": 1,
1609                "notification_count": 2,
1610            },
1611            "summary": {
1612                "room_heroes": [{
1613                    "user_id": "@somebody:example.org",
1614                    "display_name": null,
1615                    "avatar_url": null
1616                }],
1617                "joined_member_count": 5,
1618                "invited_member_count": 0,
1619            },
1620            "members_synced": true,
1621            "last_prev_batch": "pb",
1622            "sync_info": "FullySynced",
1623            "encryption_state_synced": true,
1624            "latest_event": {
1625                "event": {
1626                    "encryption_info": null,
1627                    "event": {
1628                        "sender": "@u:i.uk",
1629                    },
1630                },
1631            },
1632            "base_info": {
1633                "avatar": null,
1634                "canonical_alias": null,
1635                "create": null,
1636                "dm_targets": [],
1637                "encryption": null,
1638                "guest_access": null,
1639                "history_visibility": null,
1640                "join_rules": null,
1641                "max_power_level": 100,
1642                "name": null,
1643                "tombstone": null,
1644                "topic": null,
1645            },
1646            "read_receipts": {
1647                "num_unread": 0,
1648                "num_mentions": 0,
1649                "num_notifications": 0,
1650                "latest_active": null,
1651                "pending": []
1652            },
1653            "recency_stamp": 42,
1654        });
1655        let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1656
1657        assert_eq!(room_info.data_format_version, 0);
1658        assert!(room_info.base_info.notable_tags.is_empty());
1659        assert!(room_info.base_info.pinned_events.is_none());
1660
1661        // Apply migrations with an empty store.
1662        assert!(room_info.apply_migrations(store.clone()).await);
1663
1664        assert_eq!(room_info.data_format_version, 1);
1665        assert!(room_info.base_info.notable_tags.is_empty());
1666        assert!(room_info.base_info.pinned_events.is_none());
1667
1668        // Applying migrations again has no effect.
1669        assert!(!room_info.apply_migrations(store.clone()).await);
1670
1671        assert_eq!(room_info.data_format_version, 1);
1672        assert!(room_info.base_info.notable_tags.is_empty());
1673        assert!(room_info.base_info.pinned_events.is_none());
1674
1675        // Add events to the store.
1676        let mut changes = StateChanges::default();
1677
1678        let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1679        let mut tags = Tags::new();
1680        tags.insert(TagName::Favorite, TagInfo::new());
1681        tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1682        let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1683        let tag_event = raw_tag_event.deserialize().unwrap();
1684        changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1685
1686        let raw_pinned_events_event: Raw<_> = f
1687            .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1688            .into_raw_sync_state();
1689        let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1690        changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1691
1692        store.save_changes(&changes).await.unwrap();
1693
1694        // Reset to version 0 and reapply migrations.
1695        room_info.data_format_version = 0;
1696        assert!(room_info.apply_migrations(store.clone()).await);
1697
1698        assert_eq!(room_info.data_format_version, 1);
1699        assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1700        assert!(room_info.base_info.pinned_events.is_some());
1701
1702        // Creating a new room info initializes it to version 1.
1703        let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1704        assert_eq!(new_room_info.data_format_version, 1);
1705    }
1706
1707    #[test]
1708    fn test_room_info_deserialization() {
1709        let info_json = json!({
1710            "room_id": "!gda78o:server.tld",
1711            "room_state": "Joined",
1712            "notification_counts": {
1713                "highlight_count": 1,
1714                "notification_count": 2,
1715            },
1716            "summary": {
1717                "room_heroes": [{
1718                    "user_id": "@somebody:example.org",
1719                    "display_name": "Somebody",
1720                    "avatar_url": "mxc://example.org/abc"
1721                }],
1722                "joined_member_count": 5,
1723                "invited_member_count": 0,
1724            },
1725            "members_synced": true,
1726            "last_prev_batch": "pb",
1727            "sync_info": "FullySynced",
1728            "encryption_state_synced": true,
1729            "base_info": {
1730                "avatar": null,
1731                "canonical_alias": null,
1732                "create": null,
1733                "dm_targets": [],
1734                "encryption": null,
1735                "guest_access": null,
1736                "history_visibility": null,
1737                "join_rules": null,
1738                "max_power_level": 100,
1739                "member_hints": null,
1740                "name": null,
1741                "tombstone": null,
1742                "topic": null,
1743            },
1744            "cached_display_name": { "Calculated": "lol" },
1745            "cached_user_defined_notification_mode": "Mute",
1746            "recency_stamp": 42,
1747        });
1748
1749        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1750
1751        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1752        assert_eq!(info.room_state, RoomState::Joined);
1753        assert_eq!(info.notification_counts.highlight_count, 1);
1754        assert_eq!(info.notification_counts.notification_count, 2);
1755        assert_eq!(
1756            info.summary.room_heroes,
1757            vec![RoomHero {
1758                user_id: owned_user_id!("@somebody:example.org"),
1759                display_name: Some("Somebody".to_owned()),
1760                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1761            }]
1762        );
1763        assert_eq!(info.summary.joined_member_count, 5);
1764        assert_eq!(info.summary.invited_member_count, 0);
1765        assert!(info.members_synced);
1766        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1767        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1768        assert!(info.encryption_state_synced);
1769        assert_matches!(info.latest_event_value, LatestEventValue::None);
1770        assert!(info.base_info.avatar.is_none());
1771        assert!(info.base_info.canonical_alias.is_none());
1772        assert!(info.base_info.create.is_none());
1773        assert_eq!(info.base_info.dm_targets.len(), 0);
1774        assert!(info.base_info.encryption.is_none());
1775        assert!(info.base_info.guest_access.is_none());
1776        assert!(info.base_info.history_visibility.is_none());
1777        assert!(info.base_info.join_rules.is_none());
1778        assert_eq!(info.base_info.max_power_level, 100);
1779        assert!(info.base_info.member_hints.is_none());
1780        assert!(info.base_info.name.is_none());
1781        assert!(info.base_info.tombstone.is_none());
1782        assert!(info.base_info.topic.is_none());
1783
1784        assert_eq!(
1785            info.cached_display_name.as_ref(),
1786            Some(&RoomDisplayName::Calculated("lol".to_owned())),
1787        );
1788        assert_eq!(
1789            info.cached_user_defined_notification_mode.as_ref(),
1790            Some(&RoomNotificationMode::Mute)
1791        );
1792        assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1793    }
1794
1795    // Ensure we can still deserialize RoomInfos before we added things to its
1796    // schema
1797    //
1798    // In an ideal world, we must not change this test. Please see
1799    // [`test_room_info_serialization`] if you want to test a “recent” `RoomInfo`
1800    // deserialization.
1801    #[test]
1802    fn test_room_info_deserialization_without_optional_items() {
1803        // The following JSON should never change if we want to be able to read in old
1804        // cached state
1805        let info_json = json!({
1806            "room_id": "!gda78o:server.tld",
1807            "room_state": "Invited",
1808            "notification_counts": {
1809                "highlight_count": 1,
1810                "notification_count": 2,
1811            },
1812            "summary": {
1813                "room_heroes": [{
1814                    "user_id": "@somebody:example.org",
1815                    "display_name": "Somebody",
1816                    "avatar_url": "mxc://example.org/abc"
1817                }],
1818                "joined_member_count": 5,
1819                "invited_member_count": 0,
1820            },
1821            "members_synced": true,
1822            "last_prev_batch": "pb",
1823            "sync_info": "FullySynced",
1824            "encryption_state_synced": true,
1825            "base_info": {
1826                "avatar": null,
1827                "canonical_alias": null,
1828                "create": null,
1829                "dm_targets": [],
1830                "encryption": null,
1831                "guest_access": null,
1832                "history_visibility": null,
1833                "join_rules": null,
1834                "max_power_level": 100,
1835                "name": null,
1836                "tombstone": null,
1837                "topic": null,
1838            },
1839        });
1840
1841        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1842
1843        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1844        assert_eq!(info.room_state, RoomState::Invited);
1845        assert_eq!(info.notification_counts.highlight_count, 1);
1846        assert_eq!(info.notification_counts.notification_count, 2);
1847        assert_eq!(
1848            info.summary.room_heroes,
1849            vec![RoomHero {
1850                user_id: owned_user_id!("@somebody:example.org"),
1851                display_name: Some("Somebody".to_owned()),
1852                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1853            }]
1854        );
1855        assert_eq!(info.summary.joined_member_count, 5);
1856        assert_eq!(info.summary.invited_member_count, 0);
1857        assert!(info.members_synced);
1858        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1859        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1860        assert!(info.encryption_state_synced);
1861        assert!(info.base_info.avatar.is_none());
1862        assert!(info.base_info.canonical_alias.is_none());
1863        assert!(info.base_info.create.is_none());
1864        assert_eq!(info.base_info.dm_targets.len(), 0);
1865        assert!(info.base_info.encryption.is_none());
1866        assert!(info.base_info.guest_access.is_none());
1867        assert!(info.base_info.history_visibility.is_none());
1868        assert!(info.base_info.join_rules.is_none());
1869        assert_eq!(info.base_info.max_power_level, 100);
1870        assert!(info.base_info.name.is_none());
1871        assert!(info.base_info.tombstone.is_none());
1872        assert!(info.base_info.topic.is_none());
1873    }
1874
1875    #[test]
1876    fn test_member_hints_with_different_contents_reset_computed_value() {
1877        let expected = BTreeSet::from_iter([
1878            owned_user_id!("@alice:example.org"),
1879            owned_user_id!("@bob:example.org"),
1880        ]);
1881
1882        let info_json = json!({
1883            "room_id": "!gda78o:server.tld",
1884            "room_state": "Invited",
1885            "notification_counts": {
1886                "highlight_count": 1,
1887                "notification_count": 2,
1888            },
1889            "summary": {
1890                "room_heroes": [{
1891                    "user_id": "@somebody:example.org",
1892                    "display_name": "Somebody",
1893                    "avatar_url": "mxc://example.org/abc"
1894                }],
1895                "joined_member_count": 5,
1896                "invited_member_count": 0,
1897                "active_service_members": 2,
1898            },
1899            "members_synced": true,
1900            "last_prev_batch": "pb",
1901            "sync_info": "FullySynced",
1902            "encryption_state_synced": true,
1903            "base_info": {
1904                "avatar": null,
1905                "canonical_alias": null,
1906                "create": null,
1907                "dm_targets": [],
1908                "encryption": null,
1909                "guest_access": null,
1910                "history_visibility": null,
1911                "join_rules": null,
1912                "max_power_level": 100,
1913                "member_hints": {
1914                    "Original": {
1915                        "content": {
1916                            "service_members": ["@alice:example.org", "@bob:example.org"]
1917                        }
1918                    }
1919                },
1920                "name": null,
1921                "tombstone": null,
1922                "topic": null,
1923            },
1924        });
1925
1926        let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1927        assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1928        assert_eq!(info.summary.active_service_members, Some(2));
1929
1930        // We receive a new event with the same values as the stored ones
1931        let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1932        let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1933            EventFactory::new()
1934                .sender(user_id!("@alice:example.org"))
1935                .member_hints(expected.clone())
1936                .into_raw_sync_state(),
1937        )
1938        .expect("Expected member hints event is created");
1939
1940        info.handle_state_event(&mut raw_state_event_with_keys);
1941
1942        // Nothing changed
1943        assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1944        // And the computed value is kept
1945        assert_eq!(info.summary.active_service_members, Some(2));
1946
1947        // We receive a new event with different values from the stored ones
1948        let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
1949        let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
1950        let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1951            EventFactory::new()
1952                .sender(user_id!("@alice:example.org"))
1953                .member_hints(new_member_hints.clone())
1954                .into_raw_sync_state(),
1955        )
1956        .expect("New member hints event is created");
1957
1958        info.handle_state_event(&mut raw_state_event_with_keys);
1959
1960        // The new member hints were applied
1961        assert_eq!(
1962            info.base_info.member_hints.unwrap().content.service_members.unwrap(),
1963            new_member_hints
1964        );
1965        // And the computed value is reset
1966        assert!(info.summary.active_service_members.is_none());
1967    }
1968
1969    fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
1970        let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1971        let user_id = user_id!("@user:localhost");
1972        let room_id = room_id!("!room:localhost");
1973        let (sender, _) = tokio::sync::broadcast::channel(1);
1974        let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
1975        (room, state_store)
1976    }
1977
1978    #[async_test]
1979    async fn test_update_room_info_only_updates_in_memory_room_info() {
1980        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
1981
1982        let before = room.clone_info();
1983        assert_eq!(before.state(), RoomState::Joined);
1984        room.update_room_info(|mut info| {
1985            info.mark_as_banned();
1986            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
1987        })
1988        .await;
1989        let after = room.clone_info();
1990        assert_eq!(after.state(), RoomState::Banned);
1991
1992        let infos = state_store
1993            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
1994            .await
1995            .expect("get room info");
1996        assert!(infos.is_empty());
1997    }
1998
1999    #[async_test]
2000    async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
2001        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2002
2003        let before = room.clone_info();
2004        assert_eq!(before.state(), RoomState::Joined);
2005        room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
2006            info.mark_as_banned();
2007            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2008        })
2009        .expect("update room info");
2010        let after = room.clone_info();
2011        assert_eq!(after.state(), RoomState::Banned);
2012
2013        let infos = state_store
2014            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2015            .await
2016            .expect("get room info");
2017        assert!(infos.is_empty());
2018    }
2019
2020    #[async_test]
2021    async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2022        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2023
2024        room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2025            (info, RoomInfoNotableUpdateReasons::NONE)
2026        })
2027        .expect("room accepts guard for underlying mutex");
2028
2029        let mutex = Mutex::new(());
2030        room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2031            (info, RoomInfoNotableUpdateReasons::NONE)
2032        })
2033        .expect_err("room does not accept guard for unknown mutex");
2034    }
2035
2036    #[async_test]
2037    async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2038        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2039
2040        let before = room.clone_info();
2041        assert_eq!(before.state(), RoomState::Joined);
2042        room.update_and_save_room_info(|mut info| {
2043            info.mark_as_banned();
2044            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2045        })
2046        .await
2047        .expect("update and save room info");
2048        let after = room.clone_info();
2049        assert_eq!(after.state(), RoomState::Banned);
2050
2051        let infos = state_store
2052            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2053            .await
2054            .expect("get room info");
2055        assert_eq!(infos.len(), 1);
2056        assert_matches!(infos.first(), Some(info) => {
2057            info.state() == RoomState::Banned
2058        });
2059    }
2060
2061    #[async_test]
2062    async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2063    {
2064        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2065
2066        let before = room.clone_info();
2067        assert_eq!(before.state(), RoomState::Joined);
2068        room.update_and_save_room_info_with_store_guard(
2069            &state_store.lock().lock().await,
2070            |mut info| {
2071                info.mark_as_banned();
2072                (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2073            },
2074        )
2075        .await
2076        .expect("update and save room info");
2077        let after = room.clone_info();
2078        assert_eq!(after.state(), RoomState::Banned);
2079
2080        let infos = state_store
2081            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2082            .await
2083            .expect("get room info");
2084        assert_eq!(infos.len(), 1);
2085        assert_matches!(infos.first(), Some(info) => {
2086            info.state() == RoomState::Banned
2087        });
2088    }
2089
2090    #[async_test]
2091    async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2092        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2093
2094        room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2095            (info, RoomInfoNotableUpdateReasons::NONE)
2096        })
2097        .await
2098        .expect("room accepts guard for underlying mutex");
2099
2100        let mutex = Mutex::new(());
2101        room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2102            (info, RoomInfoNotableUpdateReasons::NONE)
2103        })
2104        .await
2105        .expect_err("room does not accept guard for unknown mutex");
2106    }
2107
2108    #[derive(Debug)]
2109    struct Elapsed;
2110
2111    async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2112        #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2113        {
2114            match future::select(sleep(duration), f).await {
2115                Either::Left(_) => return Err(Elapsed),
2116                Either::Right((output, _)) => Ok(output),
2117            }
2118        }
2119        #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2120        {
2121            tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2122        }
2123    }
2124
2125    #[async_test]
2126    async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2127        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2128
2129        // Acquire lock and hold it for 5 seconds
2130        let lock_task = spawn({
2131            let state_store = state_store.clone();
2132            async move {
2133                let lock = state_store.lock();
2134                let _guard = lock.lock().await;
2135                sleep(Duration::from_secs(5)).await;
2136            }
2137        });
2138
2139        // Try to update room info while the lock is held by another task
2140        let save_task = spawn(async move {
2141            room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2142        });
2143
2144        // Ensure that the second task does not progress until the first task has
2145        // completed and, therefore, releases the save lock
2146        assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2147            timeout(Duration::from_millis(100), save_task)
2148                .await
2149                .expect("task completes before timeout")
2150                .expect("task completes successfully")
2151        });
2152    }
2153
2154    #[async_test]
2155    async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2156        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2157
2158        // Acquire lock and hold it for 5 seconds
2159        let lock_task = spawn({
2160            let state_store = state_store.clone();
2161            async move {
2162                let lock = state_store.lock();
2163                let _guard = lock.lock().await;
2164                sleep(Duration::from_secs(5)).await;
2165            }
2166        });
2167
2168        // Try to update room info while the lock is held by another task
2169        let save_task = spawn(async move {
2170            room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2171        });
2172
2173        // Ensure that the second task does not progress until the first task has
2174        // completed and, therefore, releases the save lock
2175        assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2176            timeout(Duration::from_millis(100), save_task)
2177                .await
2178                .expect("task completes before timeout")
2179                .expect("task completes successfully")
2180                .expect("update and save room info");
2181        });
2182    }
2183}