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