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