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    /// Whether the given `(user_id, device_id)` tuple is currently a
1141    /// participant in this room's active MatrixRTC call.
1142    ///
1143    /// Distinct from [`Self::active_room_call_participants`] which returns
1144    /// only user IDs. Callers that must not conflate multiple devices of
1145    /// the same user (e.g. profile-field mirroring) should use this.
1146    pub fn is_device_in_active_room_call(
1147        &self,
1148        user_id: &ruma::UserId,
1149        device_id: &ruma::DeviceId,
1150    ) -> bool {
1151        self.active_room_call_memberships().iter().any(|(state_key, membership)| {
1152            state_key.user_id() == user_id && membership.device_id() == device_id
1153        })
1154    }
1155
1156    /// Get the call intent consensus for the current call, based on what
1157    /// members are advertising.
1158    ///
1159    /// This provides detailed information about the consensus state (is it an
1160    /// audio or video call), including whether it's full (all members
1161    /// agree) or partial (only some members advertise), allowing callers to
1162    /// distinguish between different levels of consensus.
1163    ///
1164    /// # Returns
1165    ///
1166    /// - [`CallIntentConsensus::Full`] if all members advertise and agree on
1167    ///   the same intent
1168    /// - [`CallIntentConsensus::Partial`] if only some members advertise but
1169    ///   those who do agree
1170    /// - [`CallIntentConsensus::None`] if no one advertises or advertisers
1171    ///   disagree
1172    pub fn active_room_call_consensus_intent(&self) -> CallIntentConsensus {
1173        let memberships = self.active_room_call_memberships();
1174        let total_count: u64 = memberships.len() as u64;
1175
1176        if total_count == 0 {
1177            return CallIntentConsensus::None;
1178        }
1179
1180        // Track the first intent found and count how many members advertise it
1181        let mut consensus_intent: Option<CallIntent> = None;
1182        let mut agreeing_count: u64 = 0;
1183
1184        for (_, data) in memberships.iter() {
1185            if let Some(intent) = data.call_intent() {
1186                match &consensus_intent {
1187                    // First intent found, set it as consensus
1188                    None => {
1189                        consensus_intent = Some(intent.clone());
1190                        agreeing_count = 1;
1191                    }
1192                    // Check if this intent matches the consensus
1193                    Some(current) if current == intent => {
1194                        agreeing_count += 1;
1195                    }
1196                    // Intents differ, no consensus
1197                    Some(_) => return CallIntentConsensus::None,
1198                }
1199            }
1200        }
1201
1202        // Return the appropriate consensus type based on participation
1203        match consensus_intent {
1204            None => CallIntentConsensus::None,
1205            Some(intent) if agreeing_count == total_count => {
1206                // All members advertise and agree
1207                CallIntentConsensus::Full(intent)
1208            }
1209            Some(intent) => {
1210                // Some members advertise and agree, others don't advertise
1211                CallIntentConsensus::Partial { intent, agreeing_count, total_count }
1212            }
1213        }
1214    }
1215
1216    /// Returns a Vec of userId's that participate in the room call.
1217    ///
1218    /// matrix_rtc memberships with application "m.call" and scope "m.room" are
1219    /// considered. A user can occur twice if they join with two devices.
1220    /// convert to a set depending if the different users are required or the
1221    /// amount of sessions.
1222    ///
1223    /// The vector is ordered by oldest membership user to newest.
1224    pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1225        self.active_room_call_memberships()
1226            .iter()
1227            .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1228            .collect()
1229    }
1230
1231    /// Sets the new [`LatestEventValue`].
1232    pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1233        self.latest_event_value = new_value;
1234    }
1235
1236    /// Updates the recency stamp of this room.
1237    ///
1238    /// Please read `Self::recency_stamp` to learn more.
1239    pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1240        self.recency_stamp = Some(stamp);
1241    }
1242
1243    /// Returns the current pinned event ids for this room.
1244    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1245        self.base_info.pinned_events.clone().and_then(|c| c.pinned)
1246    }
1247
1248    /// Returns the event ID of the user's `m.fully_read` marker for this room,
1249    /// if any.
1250    pub fn fully_read_event_id(&self) -> Option<&EventId> {
1251        self.base_info.fully_read_event_id.as_deref()
1252    }
1253
1254    /// Checks if an `EventId` is currently pinned.
1255    /// It avoids having to clone the whole list of event ids to check a single
1256    /// value.
1257    ///
1258    /// Returns `true` if the provided `event_id` is pinned, `false` otherwise.
1259    pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1260        self.base_info
1261            .pinned_events
1262            .as_ref()
1263            .and_then(|content| content.pinned.as_deref())
1264            .is_some_and(|pinned| pinned.contains(&event_id.to_owned()))
1265    }
1266
1267    /// Returns the computed read receipts for this room.
1268    pub fn read_receipts(&self) -> &RoomReadReceipts {
1269        &self.read_receipts
1270    }
1271
1272    /// Set the computed read receipts for this room.
1273    pub fn set_read_receipts(&mut self, read_receipts: RoomReadReceipts) {
1274        self.read_receipts = read_receipts;
1275    }
1276
1277    /// Apply migrations to this `RoomInfo` if needed.
1278    ///
1279    /// This should be used to populate new fields with data from the state
1280    /// store.
1281    ///
1282    /// Returns `true` if migrations were applied and this `RoomInfo` needs to
1283    /// be persisted to the state store.
1284    #[instrument(skip_all, fields(room_id = ?self.room_id))]
1285    pub(crate) async fn apply_migrations(&mut self, store: SaveLockedStateStore) -> bool {
1286        let mut migrated = false;
1287
1288        if self.data_format_version < 1 {
1289            info!("Migrating room info to version 1");
1290
1291            // notable_tags
1292            match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1293                // Pinned events are never in stripped state.
1294                Ok(Some(raw_event)) => match raw_event.deserialize() {
1295                    Ok(event) => {
1296                        self.base_info.handle_notable_tags(&event.content.tags);
1297                    }
1298                    Err(error) => {
1299                        warn!("Failed to deserialize room tags: {error}");
1300                    }
1301                },
1302                Ok(_) => {
1303                    // Nothing to do.
1304                }
1305                Err(error) => {
1306                    warn!("Failed to load room tags: {error}");
1307                }
1308            }
1309
1310            // pinned_events
1311            match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1312            {
1313                // Pinned events are never in stripped state.
1314                Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1315                    if let Some(mut raw_event) =
1316                        RawStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1317                    {
1318                        self.handle_state_event(&mut raw_event);
1319                    }
1320                }
1321                Ok(_) => {
1322                    // Nothing to do.
1323                }
1324                Err(error) => {
1325                    warn!("Failed to load room pinned events: {error}");
1326                }
1327            }
1328
1329            self.data_format_version = 1;
1330            migrated = true;
1331        }
1332
1333        migrated
1334    }
1335
1336    /// Returns the number of active (joined/invited) service members in the
1337    /// room, if known.
1338    pub fn active_service_member_count(&self) -> Option<u64> {
1339        self.summary.active_service_members
1340    }
1341
1342    /// Updates the cached value for the number of active service members in the
1343    /// room.
1344    pub fn update_active_service_member_count(&mut self, count: Option<u64>) {
1345        self.summary.active_service_members = count;
1346    }
1347}
1348
1349/// Type to represent a `RoomInfo::recency_stamp`.
1350#[repr(transparent)]
1351#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1352#[serde(transparent)]
1353pub struct RoomRecencyStamp(u64);
1354
1355impl From<u64> for RoomRecencyStamp {
1356    fn from(value: u64) -> Self {
1357        Self(value)
1358    }
1359}
1360
1361impl From<RoomRecencyStamp> for u64 {
1362    fn from(value: RoomRecencyStamp) -> Self {
1363        value.0
1364    }
1365}
1366
1367#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1368pub(crate) enum SyncInfo {
1369    /// We only know the room exists and whether it is in invite / joined / left
1370    /// state.
1371    ///
1372    /// This is the case when we have a limited sync or only seen the room
1373    /// because of a request we've done, like a room creation event.
1374    NoState,
1375
1376    /// Some states have been synced, but they might have been filtered or is
1377    /// stale, as it is from a room we've left.
1378    PartiallySynced,
1379
1380    /// We have all the latest state events.
1381    FullySynced,
1382}
1383
1384/// Apply a redaction to the given target `event`, given the raw redaction event
1385/// and the room version.
1386pub fn apply_redaction(
1387    event: &Raw<AnySyncTimelineEvent>,
1388    raw_redaction: &Raw<SyncRoomRedactionEvent>,
1389    rules: &RedactionRules,
1390) -> Option<Raw<AnySyncTimelineEvent>> {
1391    use ruma::canonical_json::{RedactedBecause, redact_in_place};
1392
1393    let mut event_json = match event.deserialize_as() {
1394        Ok(json) => json,
1395        Err(e) => {
1396            warn!("Failed to deserialize latest event: {e}");
1397            return None;
1398        }
1399    };
1400
1401    let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1402        Ok(rb) => rb,
1403        Err(e) => {
1404            warn!("Redaction event is not valid canonical JSON: {e}");
1405            return None;
1406        }
1407    };
1408
1409    let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1410
1411    if let Err(e) = redact_result {
1412        warn!("Failed to redact event: {e}");
1413        return None;
1414    }
1415
1416    let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1417    Some(raw.cast_unchecked())
1418}
1419
1420/// Indicates that a notable update of `RoomInfo` has been applied, and why.
1421///
1422/// A room info notable update is an update that can be interesting for other
1423/// parts of the code. This mechanism is used in coordination with
1424/// [`BaseClient::room_info_notable_update_receiver`][baseclient] (and
1425/// `Room::info` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
1426/// can be observed and some of its updates can be spread to listeners.
1427///
1428/// [baseclient]: crate::BaseClient::room_info_notable_update_receiver
1429#[derive(Debug, Clone)]
1430pub struct RoomInfoNotableUpdate {
1431    /// The room which was updated.
1432    pub room_id: OwnedRoomId,
1433
1434    /// The reason for this update.
1435    pub reasons: RoomInfoNotableUpdateReasons,
1436}
1437
1438bitflags! {
1439    /// The reason why a [`RoomInfoNotableUpdate`] is emitted.
1440    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1441    pub struct RoomInfoNotableUpdateReasons: u16 {
1442        /// The recency stamp of the `Room` has changed.
1443        const RECENCY_STAMP = 0b0000_0000_0000_0001;
1444
1445        /// The latest event of the `Room` has changed.
1446        const LATEST_EVENT = 0b0000_0000_0000_0010;
1447
1448        /// A read receipt has changed.
1449        const READ_RECEIPT = 0b0000_0000_0000_0100;
1450
1451        /// The user-controlled unread marker value has changed.
1452        const UNREAD_MARKER = 0b0000_0000_0000_1000;
1453
1454        /// A membership change happened for the current user.
1455        const MEMBERSHIP = 0b0000_0000_0001_0000;
1456
1457        /// The display name has changed.
1458        const DISPLAY_NAME = 0b0000_0000_0010_0000;
1459
1460        /// The active service members have changed.
1461        const ACTIVE_SERVICE_MEMBERS = 0b0000_0000_0100_0000;
1462
1463        /// This is a temporary hack.
1464        ///
1465        /// So here is the thing. Ideally, we DO NOT want to emit this reason. It does not
1466        /// makes sense. However, all notable update reasons are not clearly identified
1467        /// so far. Why is it a problem? The `matrix_sdk_ui::room_list_service::RoomList`
1468        /// is listening this stream of [`RoomInfoNotableUpdate`], and emits an update on a
1469        /// room item if it receives a notable reason. Because all reasons are not
1470        /// identified, we are likely to miss particular updates, and it can feel broken.
1471        /// Ultimately, we want to clearly identify all the notable update reasons, and
1472        /// remove this one.
1473        const NONE = 0b0000_0000_1000_0000;
1474
1475        /// The user's `m.fully_read` marker has changed.
1476        const FULLY_READ = 0b0000_0001_0000_0000;
1477
1478        /// A room hero's global profile changed (e.g. their status or call).
1479        const HEROES = 0b0000_0010_0000_0000;
1480    }
1481}
1482
1483impl Default for RoomInfoNotableUpdateReasons {
1484    fn default() -> Self {
1485        Self::empty()
1486    }
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491    use std::{collections::BTreeSet, str::FromStr, sync::Arc, time::Duration};
1492
1493    use assert_matches::assert_matches;
1494    use futures_util::future::{self, Either};
1495    #[cfg(all(target_family = "wasm", target_os = "unknown"))]
1496    use gloo_timers::future::sleep;
1497    use matrix_sdk_common::executor::spawn;
1498    use matrix_sdk_test::{async_test, event_factory::EventFactory};
1499    use ruma::{
1500        assign,
1501        events::{
1502            AnyRoomAccountDataEvent,
1503            room::pinned_events::RoomPinnedEventsEventContent,
1504            tag::{TagInfo, TagName, Tags, UserTagName},
1505        },
1506        owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1507        serde::Raw,
1508        user_id,
1509    };
1510    use serde_json::json;
1511    use similar_asserts::assert_eq;
1512    use tokio::sync::Mutex;
1513    #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
1514    use tokio::time::sleep;
1515
1516    use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1517    use crate::{
1518        RawStateEventWithKeys, Room, RoomDisplayName, RoomHero, RoomInfoNotableUpdateReasons,
1519        RoomState, StateChanges, StateStore,
1520        notification_settings::RoomNotificationMode,
1521        room::{RoomNotableTags, RoomSummary},
1522        store::{IntoStateStore, MemoryStore, RoomLoadSettings, SaveLockedStateStore},
1523        sync::UnreadNotificationsCount,
1524    };
1525
1526    #[test]
1527    fn test_room_info_serialization() {
1528        // This test exists to make sure we don't accidentally change the
1529        // serialized format for `RoomInfo`.
1530
1531        let info = RoomInfo {
1532            data_format_version: 1,
1533            room_id: room_id!("!gda78o:server.tld").into(),
1534            room_state: RoomState::Invited,
1535            notification_counts: UnreadNotificationsCount {
1536                highlight_count: 1,
1537                notification_count: 2,
1538            },
1539            summary: RoomSummary {
1540                room_heroes: vec![RoomHero {
1541                    user_id: owned_user_id!("@somebody:example.org"),
1542                    display_name: None,
1543                    avatar_url: None,
1544                }],
1545                joined_member_count: 5,
1546                invited_member_count: 0,
1547                active_service_members: None,
1548            },
1549            members_synced: true,
1550            last_prev_batch: Some("pb".to_owned()),
1551            sync_info: SyncInfo::FullySynced,
1552            encryption_state_synced: true,
1553            latest_event_value: LatestEventValue::None,
1554            base_info: Box::new(
1555                assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")]).into()) }),
1556            ),
1557            read_receipts: Default::default(),
1558            warned_about_unknown_room_version_rules: Arc::new(false.into()),
1559            cached_display_name: None,
1560            cached_user_defined_notification_mode: None,
1561            recency_stamp: Some(42.into()),
1562        };
1563
1564        let info_json = json!({
1565            "data_format_version": 1,
1566            "room_id": "!gda78o:server.tld",
1567            "room_state": "Invited",
1568            "notification_counts": {
1569                "highlight_count": 1,
1570                "notification_count": 2,
1571            },
1572            "summary": {
1573                "room_heroes": [{
1574                    "user_id": "@somebody:example.org",
1575                    "display_name": null,
1576                    "avatar_url": null
1577                }],
1578                "joined_member_count": 5,
1579                "invited_member_count": 0,
1580            },
1581            "members_synced": true,
1582            "last_prev_batch": "pb",
1583            "sync_info": "FullySynced",
1584            "encryption_state_synced": true,
1585            "latest_event_value": "None",
1586            "base_info": {
1587                "avatar": null,
1588                "canonical_alias": null,
1589                "create": null,
1590                "dm_targets": [],
1591                "encryption": null,
1592                "guest_access": null,
1593                "history_visibility": null,
1594                "is_marked_unread": false,
1595                "is_marked_unread_source": "Unstable",
1596                "join_rules": null,
1597                "max_power_level": 100,
1598                "member_hints": null,
1599                "name": null,
1600                "tombstone": null,
1601                "topic": null,
1602                "pinned_events": {
1603                    "pinned": ["$a"]
1604                },
1605            },
1606            "read_receipts": {
1607                "num_unread": 0,
1608                "num_mentions": 0,
1609                "num_notifications": 0,
1610                "latest_active": null,
1611                "pending": {
1612                    "items": [],
1613                    "capacity": 10,
1614                },
1615            },
1616            "recency_stamp": 42,
1617        });
1618
1619        assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1620    }
1621
1622    #[async_test]
1623    async fn test_room_info_migration_v1() {
1624        let store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1625
1626        let room_info_json = json!({
1627            "room_id": "!gda78o:server.tld",
1628            "room_state": "Joined",
1629            "notification_counts": {
1630                "highlight_count": 1,
1631                "notification_count": 2,
1632            },
1633            "summary": {
1634                "room_heroes": [{
1635                    "user_id": "@somebody:example.org",
1636                    "display_name": null,
1637                    "avatar_url": null
1638                }],
1639                "joined_member_count": 5,
1640                "invited_member_count": 0,
1641            },
1642            "members_synced": true,
1643            "last_prev_batch": "pb",
1644            "sync_info": "FullySynced",
1645            "encryption_state_synced": true,
1646            "latest_event": {
1647                "event": {
1648                    "encryption_info": null,
1649                    "event": {
1650                        "sender": "@u:i.uk",
1651                    },
1652                },
1653            },
1654            "base_info": {
1655                "avatar": null,
1656                "canonical_alias": null,
1657                "create": null,
1658                "dm_targets": [],
1659                "encryption": null,
1660                "guest_access": null,
1661                "history_visibility": null,
1662                "join_rules": null,
1663                "max_power_level": 100,
1664                "name": null,
1665                "tombstone": null,
1666                "topic": null,
1667            },
1668            "read_receipts": {
1669                "num_unread": 0,
1670                "num_mentions": 0,
1671                "num_notifications": 0,
1672                "latest_active": null,
1673                "pending": []
1674            },
1675            "recency_stamp": 42,
1676        });
1677        let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1678
1679        assert_eq!(room_info.data_format_version, 0);
1680        assert!(room_info.base_info.notable_tags.is_empty());
1681        assert!(room_info.base_info.pinned_events.is_none());
1682
1683        // Apply migrations with an empty store.
1684        assert!(room_info.apply_migrations(store.clone()).await);
1685
1686        assert_eq!(room_info.data_format_version, 1);
1687        assert!(room_info.base_info.notable_tags.is_empty());
1688        assert!(room_info.base_info.pinned_events.is_none());
1689
1690        // Applying migrations again has no effect.
1691        assert!(!room_info.apply_migrations(store.clone()).await);
1692
1693        assert_eq!(room_info.data_format_version, 1);
1694        assert!(room_info.base_info.notable_tags.is_empty());
1695        assert!(room_info.base_info.pinned_events.is_none());
1696
1697        // Add events to the store.
1698        let mut changes = StateChanges::default();
1699
1700        let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1701        let mut tags = Tags::new();
1702        tags.insert(TagName::Favorite, TagInfo::new());
1703        tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1704        let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1705        let tag_event = raw_tag_event.deserialize().unwrap();
1706        changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1707
1708        let raw_pinned_events_event: Raw<_> = f
1709            .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1710            .into_raw_sync_state();
1711        let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1712        changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1713
1714        store.save_changes(&changes).await.unwrap();
1715
1716        // Reset to version 0 and reapply migrations.
1717        room_info.data_format_version = 0;
1718        assert!(room_info.apply_migrations(store.clone()).await);
1719
1720        assert_eq!(room_info.data_format_version, 1);
1721        assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1722        assert!(room_info.base_info.pinned_events.is_some());
1723
1724        // Creating a new room info initializes it to version 1.
1725        let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1726        assert_eq!(new_room_info.data_format_version, 1);
1727    }
1728
1729    #[test]
1730    fn test_room_info_deserialization() {
1731        let info_json = json!({
1732            "room_id": "!gda78o:server.tld",
1733            "room_state": "Joined",
1734            "notification_counts": {
1735                "highlight_count": 1,
1736                "notification_count": 2,
1737            },
1738            "summary": {
1739                "room_heroes": [{
1740                    "user_id": "@somebody:example.org",
1741                    "display_name": "Somebody",
1742                    "avatar_url": "mxc://example.org/abc"
1743                }],
1744                "joined_member_count": 5,
1745                "invited_member_count": 0,
1746            },
1747            "members_synced": true,
1748            "last_prev_batch": "pb",
1749            "sync_info": "FullySynced",
1750            "encryption_state_synced": true,
1751            "base_info": {
1752                "avatar": null,
1753                "canonical_alias": null,
1754                "create": null,
1755                "dm_targets": [],
1756                "encryption": null,
1757                "guest_access": null,
1758                "history_visibility": null,
1759                "join_rules": null,
1760                "max_power_level": 100,
1761                "member_hints": null,
1762                "name": null,
1763                "tombstone": null,
1764                "topic": null,
1765            },
1766            "cached_display_name": { "Calculated": "lol" },
1767            "cached_user_defined_notification_mode": "Mute",
1768            "recency_stamp": 42,
1769        });
1770
1771        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1772
1773        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1774        assert_eq!(info.room_state, RoomState::Joined);
1775        assert_eq!(info.notification_counts.highlight_count, 1);
1776        assert_eq!(info.notification_counts.notification_count, 2);
1777        assert_eq!(
1778            info.summary.room_heroes,
1779            vec![RoomHero {
1780                user_id: owned_user_id!("@somebody:example.org"),
1781                display_name: Some("Somebody".to_owned()),
1782                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1783            }]
1784        );
1785        assert_eq!(info.summary.joined_member_count, 5);
1786        assert_eq!(info.summary.invited_member_count, 0);
1787        assert!(info.members_synced);
1788        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1789        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1790        assert!(info.encryption_state_synced);
1791        assert_matches!(info.latest_event_value, LatestEventValue::None);
1792        assert!(info.base_info.avatar.is_none());
1793        assert!(info.base_info.canonical_alias.is_none());
1794        assert!(info.base_info.create.is_none());
1795        assert_eq!(info.base_info.dm_targets.len(), 0);
1796        assert!(info.base_info.encryption.is_none());
1797        assert!(info.base_info.guest_access.is_none());
1798        assert!(info.base_info.history_visibility.is_none());
1799        assert!(info.base_info.join_rules.is_none());
1800        assert_eq!(info.base_info.max_power_level, 100);
1801        assert!(info.base_info.member_hints.is_none());
1802        assert!(info.base_info.name.is_none());
1803        assert!(info.base_info.tombstone.is_none());
1804        assert!(info.base_info.topic.is_none());
1805
1806        assert_eq!(
1807            info.cached_display_name.as_ref(),
1808            Some(&RoomDisplayName::Calculated("lol".to_owned())),
1809        );
1810        assert_eq!(
1811            info.cached_user_defined_notification_mode.as_ref(),
1812            Some(&RoomNotificationMode::Mute)
1813        );
1814        assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1815    }
1816
1817    // Ensure we can still deserialize RoomInfos before we added things to its
1818    // schema
1819    //
1820    // In an ideal world, we must not change this test. Please see
1821    // [`test_room_info_serialization`] if you want to test a “recent” `RoomInfo`
1822    // deserialization.
1823    #[test]
1824    fn test_room_info_deserialization_without_optional_items() {
1825        // The following JSON should never change if we want to be able to read in old
1826        // cached state
1827        let info_json = json!({
1828            "room_id": "!gda78o:server.tld",
1829            "room_state": "Invited",
1830            "notification_counts": {
1831                "highlight_count": 1,
1832                "notification_count": 2,
1833            },
1834            "summary": {
1835                "room_heroes": [{
1836                    "user_id": "@somebody:example.org",
1837                    "display_name": "Somebody",
1838                    "avatar_url": "mxc://example.org/abc"
1839                }],
1840                "joined_member_count": 5,
1841                "invited_member_count": 0,
1842            },
1843            "members_synced": true,
1844            "last_prev_batch": "pb",
1845            "sync_info": "FullySynced",
1846            "encryption_state_synced": true,
1847            "base_info": {
1848                "avatar": null,
1849                "canonical_alias": null,
1850                "create": null,
1851                "dm_targets": [],
1852                "encryption": null,
1853                "guest_access": null,
1854                "history_visibility": null,
1855                "join_rules": null,
1856                "max_power_level": 100,
1857                "name": null,
1858                "tombstone": null,
1859                "topic": null,
1860            },
1861        });
1862
1863        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1864
1865        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1866        assert_eq!(info.room_state, RoomState::Invited);
1867        assert_eq!(info.notification_counts.highlight_count, 1);
1868        assert_eq!(info.notification_counts.notification_count, 2);
1869        assert_eq!(
1870            info.summary.room_heroes,
1871            vec![RoomHero {
1872                user_id: owned_user_id!("@somebody:example.org"),
1873                display_name: Some("Somebody".to_owned()),
1874                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1875            }]
1876        );
1877        assert_eq!(info.summary.joined_member_count, 5);
1878        assert_eq!(info.summary.invited_member_count, 0);
1879        assert!(info.members_synced);
1880        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1881        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1882        assert!(info.encryption_state_synced);
1883        assert!(info.base_info.avatar.is_none());
1884        assert!(info.base_info.canonical_alias.is_none());
1885        assert!(info.base_info.create.is_none());
1886        assert_eq!(info.base_info.dm_targets.len(), 0);
1887        assert!(info.base_info.encryption.is_none());
1888        assert!(info.base_info.guest_access.is_none());
1889        assert!(info.base_info.history_visibility.is_none());
1890        assert!(info.base_info.join_rules.is_none());
1891        assert_eq!(info.base_info.max_power_level, 100);
1892        assert!(info.base_info.name.is_none());
1893        assert!(info.base_info.tombstone.is_none());
1894        assert!(info.base_info.topic.is_none());
1895    }
1896
1897    #[test]
1898    fn test_member_hints_with_different_contents_reset_computed_value() {
1899        let expected = BTreeSet::from_iter([
1900            owned_user_id!("@alice:example.org"),
1901            owned_user_id!("@bob:example.org"),
1902        ]);
1903
1904        let info_json = json!({
1905            "room_id": "!gda78o:server.tld",
1906            "room_state": "Invited",
1907            "notification_counts": {
1908                "highlight_count": 1,
1909                "notification_count": 2,
1910            },
1911            "summary": {
1912                "room_heroes": [{
1913                    "user_id": "@somebody:example.org",
1914                    "display_name": "Somebody",
1915                    "avatar_url": "mxc://example.org/abc"
1916                }],
1917                "joined_member_count": 5,
1918                "invited_member_count": 0,
1919                "active_service_members": 2,
1920            },
1921            "members_synced": true,
1922            "last_prev_batch": "pb",
1923            "sync_info": "FullySynced",
1924            "encryption_state_synced": true,
1925            "base_info": {
1926                "avatar": null,
1927                "canonical_alias": null,
1928                "create": null,
1929                "dm_targets": [],
1930                "encryption": null,
1931                "guest_access": null,
1932                "history_visibility": null,
1933                "join_rules": null,
1934                "max_power_level": 100,
1935                "member_hints": {
1936                    "Original": {
1937                        "content": {
1938                            "service_members": ["@alice:example.org", "@bob:example.org"]
1939                        }
1940                    }
1941                },
1942                "name": null,
1943                "tombstone": null,
1944                "topic": null,
1945            },
1946        });
1947
1948        let info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1949        assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1950        assert_eq!(info.summary.active_service_members, Some(2));
1951
1952        // We receive a new event with the same values as the stored ones
1953        let mut info: RoomInfo = serde_json::from_value(info_json.clone()).unwrap();
1954        let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1955            EventFactory::new()
1956                .sender(user_id!("@alice:example.org"))
1957                .member_hints(expected.clone())
1958                .into_raw_sync_state(),
1959        )
1960        .expect("Expected member hints event is created");
1961
1962        info.handle_state_event(&mut raw_state_event_with_keys);
1963
1964        // Nothing changed
1965        assert_eq!(info.base_info.member_hints.unwrap().content.service_members.unwrap(), expected);
1966        // And the computed value is kept
1967        assert_eq!(info.summary.active_service_members, Some(2));
1968
1969        // We receive a new event with different values from the stored ones
1970        let mut info: RoomInfo = serde_json::from_value(info_json).unwrap();
1971        let new_member_hints = BTreeSet::from_iter([owned_user_id!("@alice:example.org")]);
1972        let mut raw_state_event_with_keys = RawStateEventWithKeys::try_from_raw_state_event(
1973            EventFactory::new()
1974                .sender(user_id!("@alice:example.org"))
1975                .member_hints(new_member_hints.clone())
1976                .into_raw_sync_state(),
1977        )
1978        .expect("New member hints event is created");
1979
1980        info.handle_state_event(&mut raw_state_event_with_keys);
1981
1982        // The new member hints were applied
1983        assert_eq!(
1984            info.base_info.member_hints.unwrap().content.service_members.unwrap(),
1985            new_member_hints
1986        );
1987        // And the computed value is reset
1988        assert!(info.summary.active_service_members.is_none());
1989    }
1990
1991    fn make_room_and_state_store(room_state: RoomState) -> (Room, SaveLockedStateStore) {
1992        let state_store = SaveLockedStateStore::new(MemoryStore::new().into_state_store());
1993        let user_id = user_id!("@user:localhost");
1994        let room_id = room_id!("!room:localhost");
1995        let (sender, _) = tokio::sync::broadcast::channel(1);
1996        let room = Room::new(user_id, state_store.clone(), room_id, room_state, sender);
1997        (room, state_store)
1998    }
1999
2000    #[async_test]
2001    async fn test_update_room_info_only_updates_in_memory_room_info() {
2002        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2003
2004        let before = room.clone_info();
2005        assert_eq!(before.state(), RoomState::Joined);
2006        room.update_room_info(|mut info| {
2007            info.mark_as_banned();
2008            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2009        })
2010        .await;
2011        let after = room.clone_info();
2012        assert_eq!(after.state(), RoomState::Banned);
2013
2014        let infos = state_store
2015            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2016            .await
2017            .expect("get room info");
2018        assert!(infos.is_empty());
2019    }
2020
2021    #[async_test]
2022    async fn test_update_room_info_with_store_guard_only_updates_in_memory_room_info() {
2023        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2024
2025        let before = room.clone_info();
2026        assert_eq!(before.state(), RoomState::Joined);
2027        room.update_room_info_with_store_guard(&state_store.lock().lock().await, |mut info| {
2028            info.mark_as_banned();
2029            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2030        })
2031        .expect("update room info");
2032        let after = room.clone_info();
2033        assert_eq!(after.state(), RoomState::Banned);
2034
2035        let infos = state_store
2036            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2037            .await
2038            .expect("get room info");
2039        assert!(infos.is_empty());
2040    }
2041
2042    #[async_test]
2043    async fn test_update_room_info_only_accepts_guard_for_underlying_mutex() {
2044        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2045
2046        room.update_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2047            (info, RoomInfoNotableUpdateReasons::NONE)
2048        })
2049        .expect("room accepts guard for underlying mutex");
2050
2051        let mutex = Mutex::new(());
2052        room.update_room_info_with_store_guard(&mutex.lock().await, |info| {
2053            (info, RoomInfoNotableUpdateReasons::NONE)
2054        })
2055        .expect_err("room does not accept guard for unknown mutex");
2056    }
2057
2058    #[async_test]
2059    async fn test_update_and_save_room_info_updates_room_info_in_memory_and_store() {
2060        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2061
2062        let before = room.clone_info();
2063        assert_eq!(before.state(), RoomState::Joined);
2064        room.update_and_save_room_info(|mut info| {
2065            info.mark_as_banned();
2066            (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2067        })
2068        .await
2069        .expect("update and save room info");
2070        let after = room.clone_info();
2071        assert_eq!(after.state(), RoomState::Banned);
2072
2073        let infos = state_store
2074            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2075            .await
2076            .expect("get room info");
2077        assert_eq!(infos.len(), 1);
2078        assert_matches!(infos.first(), Some(info) => {
2079            info.state() == RoomState::Banned
2080        });
2081    }
2082
2083    #[async_test]
2084    async fn test_update_and_save_room_info_with_store_guard_updates_room_info_in_memory_and_store()
2085    {
2086        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2087
2088        let before = room.clone_info();
2089        assert_eq!(before.state(), RoomState::Joined);
2090        room.update_and_save_room_info_with_store_guard(
2091            &state_store.lock().lock().await,
2092            |mut info| {
2093                info.mark_as_banned();
2094                (info, RoomInfoNotableUpdateReasons::MEMBERSHIP)
2095            },
2096        )
2097        .await
2098        .expect("update and save room info");
2099        let after = room.clone_info();
2100        assert_eq!(after.state(), RoomState::Banned);
2101
2102        let infos = state_store
2103            .get_room_infos(&RoomLoadSettings::One(room.room_id.clone()))
2104            .await
2105            .expect("get room info");
2106        assert_eq!(infos.len(), 1);
2107        assert_matches!(infos.first(), Some(info) => {
2108            info.state() == RoomState::Banned
2109        });
2110    }
2111
2112    #[async_test]
2113    async fn test_update_and_save_room_info_only_accepts_guard_for_underlying_mutex() {
2114        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2115
2116        room.update_and_save_room_info_with_store_guard(&state_store.lock().lock().await, |info| {
2117            (info, RoomInfoNotableUpdateReasons::NONE)
2118        })
2119        .await
2120        .expect("room accepts guard for underlying mutex");
2121
2122        let mutex = Mutex::new(());
2123        room.update_and_save_room_info_with_store_guard(&mutex.lock().await, |info| {
2124            (info, RoomInfoNotableUpdateReasons::NONE)
2125        })
2126        .await
2127        .expect_err("room does not accept guard for unknown mutex");
2128    }
2129
2130    #[derive(Debug)]
2131    struct Elapsed;
2132
2133    async fn timeout<F: Future + Unpin>(duration: Duration, f: F) -> Result<F::Output, Elapsed> {
2134        #[cfg(all(target_family = "wasm", target_os = "unknown"))]
2135        {
2136            match future::select(sleep(duration), f).await {
2137                Either::Left(_) => return Err(Elapsed),
2138                Either::Right((output, _)) => Ok(output),
2139            }
2140        }
2141        #[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
2142        {
2143            tokio::time::timeout(duration, f).await.map_err(|_| Elapsed)
2144        }
2145    }
2146
2147    #[async_test]
2148    async fn test_update_room_info_waits_to_acquire_lock_before_updating_room_info() {
2149        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2150
2151        // Acquire lock and hold it for 5 seconds
2152        let lock_task = spawn({
2153            let state_store = state_store.clone();
2154            async move {
2155                let lock = state_store.lock();
2156                let _guard = lock.lock().await;
2157                sleep(Duration::from_secs(5)).await;
2158            }
2159        });
2160
2161        // Try to update room info while the lock is held by another task
2162        let save_task = spawn(async move {
2163            room.update_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2164        });
2165
2166        // Ensure that the second task does not progress until the first task has
2167        // completed and, therefore, releases the save lock
2168        assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2169            timeout(Duration::from_millis(100), save_task)
2170                .await
2171                .expect("task completes before timeout")
2172                .expect("task completes successfully")
2173        });
2174    }
2175
2176    #[async_test]
2177    async fn test_update_and_save_room_info_waits_to_acquire_lock_before_updating_room_info() {
2178        let (room, state_store) = make_room_and_state_store(RoomState::Joined);
2179
2180        // Acquire lock and hold it for 5 seconds
2181        let lock_task = spawn({
2182            let state_store = state_store.clone();
2183            async move {
2184                let lock = state_store.lock();
2185                let _guard = lock.lock().await;
2186                sleep(Duration::from_secs(5)).await;
2187            }
2188        });
2189
2190        // Try to update room info while the lock is held by another task
2191        let save_task = spawn(async move {
2192            room.update_and_save_room_info(|info| (info, RoomInfoNotableUpdateReasons::NONE)).await
2193        });
2194
2195        // Ensure that the second task does not progress until the first task has
2196        // completed and, therefore, releases the save lock
2197        assert_matches!(future::select(lock_task, save_task).await, Either::Left((_, save_task)) => {
2198            timeout(Duration::from_millis(100), save_task)
2199                .await
2200                .expect("task completes before timeout")
2201                .expect("task completes successfully")
2202                .expect("update and save room info");
2203        });
2204    }
2205}