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