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    assign,
29    events::{
30        AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
31        SyncStateEvent,
32        call::member::{
33            CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
34        },
35        direct::OwnedDirectUserIdentifier,
36        member_hints::PossiblyRedactedMemberHintsEventContent,
37        room::{
38            avatar::{self, PossiblyRedactedRoomAvatarEventContent},
39            canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
40            encryption::RoomEncryptionEventContent,
41            guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
42            history_visibility::{
43                HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
44            },
45            join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
46            name::PossiblyRedactedRoomNameEventContent,
47            pinned_events::RoomPinnedEventsEventContent,
48            redaction::SyncRoomRedactionEvent,
49            tombstone::PossiblyRedactedRoomTombstoneEventContent,
50            topic::PossiblyRedactedRoomTopicEventContent,
51        },
52        tag::{TagEventContent, TagName, Tags},
53    },
54    room::RoomType,
55    room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
56    serde::Raw,
57};
58use serde::{Deserialize, Serialize};
59use tracing::{field::debug, info, instrument, warn};
60
61use super::{
62    AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
63    RoomHero, RoomNotableTags, RoomState, RoomSummary,
64};
65use crate::{
66    MinimalStateEvent,
67    deserialized_responses::RawSyncOrStrippedState,
68    latest_event::LatestEventValue,
69    notification_settings::RoomNotificationMode,
70    read_receipts::RoomReadReceipts,
71    store::{DynStateStore, StateStoreExt},
72    sync::UnreadNotificationsCount,
73    utils::RawSyncStateEventWithKeys,
74};
75
76/// The default value of the maximum power level.
77const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
78
79impl Room {
80    /// Subscribe to the inner `RoomInfo`.
81    pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
82        self.info.subscribe()
83    }
84
85    /// Clone the inner `RoomInfo`.
86    pub fn clone_info(&self) -> RoomInfo {
87        self.info.get()
88    }
89
90    /// Update the summary with given RoomInfo.
91    pub fn set_room_info(
92        &self,
93        room_info: RoomInfo,
94        room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
95    ) {
96        self.info.set(room_info);
97
98        if !room_info_notable_update_reasons.is_empty() {
99            // Ignore error if no receiver exists.
100            let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
101                room_id: self.room_id.clone(),
102                reasons: room_info_notable_update_reasons,
103            });
104        } else {
105            // TODO: remove this block!
106            // Read `RoomInfoNotableUpdateReasons::NONE` to understand why it must be
107            // removed.
108            let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
109                room_id: self.room_id.clone(),
110                reasons: RoomInfoNotableUpdateReasons::NONE,
111            });
112        }
113    }
114}
115
116/// A base room info struct that is the backbone of normal as well as stripped
117/// rooms. Holds all the state events that are important to present a room to
118/// users.
119#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct BaseRoomInfo {
121    /// The avatar URL of this room.
122    pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
123    /// The canonical alias of this room.
124    pub(crate) canonical_alias:
125        Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
126    /// The `m.room.create` event content of this room.
127    pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
128    /// A list of user ids this room is considered as direct message, if this
129    /// room is a DM.
130    pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
131    /// The `m.room.encryption` event content that enabled E2EE in this room.
132    pub(crate) encryption: Option<RoomEncryptionEventContent>,
133    /// The guest access policy of this room.
134    pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
135    /// The history visibility policy of this room.
136    pub(crate) history_visibility:
137        Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
138    /// The join rule policy of this room.
139    pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
140    /// The maximal power level that can be found in this room.
141    pub(crate) max_power_level: i64,
142    /// The member hints for the room as per MSC4171, including service members,
143    /// if available.
144    pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
145    /// The `m.room.name` of this room.
146    pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
147    /// The `m.room.tombstone` event content of this room.
148    pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
149    /// The topic of this room.
150    pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
151    /// All minimal state events that containing one or more running matrixRTC
152    /// memberships.
153    #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
154    pub(crate) rtc_member_events:
155        BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
156    /// Whether this room has been manually marked as unread.
157    #[serde(default)]
158    pub(crate) is_marked_unread: bool,
159    /// The source of is_marked_unread.
160    #[serde(default)]
161    pub(crate) is_marked_unread_source: AccountDataSource,
162    /// Some notable tags.
163    ///
164    /// We are not interested by all the tags. Some tags are more important than
165    /// others, and this field collects them.
166    #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
167    pub(crate) notable_tags: RoomNotableTags,
168    /// The `m.room.pinned_events` of this room.
169    pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
170}
171
172impl BaseRoomInfo {
173    /// Create a new, empty base room info.
174    pub fn new() -> Self {
175        Self::default()
176    }
177
178    /// Get the room version of this room.
179    ///
180    /// For room versions earlier than room version 11, if the event is
181    /// redacted, this will return the default of [`RoomVersionId::V1`].
182    pub fn room_version(&self) -> Option<&RoomVersionId> {
183        Some(&self.create.as_ref()?.content.room_version)
184    }
185
186    /// Handle a state event for this room and update our info accordingly.
187    ///
188    /// Returns true if the event modified the info, false otherwise.
189    pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
190        match (&raw_event.event_type, raw_event.state_key.as_str()) {
191            (StateEventType::RoomEncryption, "") => {
192                // No redacted or failed deserialization branch - enabling encryption cannot be
193                // undone.
194                if let Some(SyncStateEvent::Original(event)) =
195                    raw_event.deserialize_as(|any_event| {
196                        as_variant!(any_event, AnySyncStateEvent::RoomEncryption)
197                    })
198                {
199                    self.encryption = Some(event.content.clone());
200                    true
201                } else {
202                    false
203                }
204            }
205            (StateEventType::RoomAvatar, "") => {
206                if let Some(event) = raw_event.deserialize_as(|any_event| {
207                    as_variant!(any_event, AnySyncStateEvent::RoomAvatar)
208                }) {
209                    self.avatar = Some(event.into());
210                    true
211                } else {
212                    // Remove the previous content if the new content is unknown.
213                    self.avatar.take().is_some()
214                }
215            }
216            (StateEventType::RoomName, "") => {
217                if let Some(event) = raw_event
218                    .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomName))
219                {
220                    self.name = Some(event.into());
221                    true
222                } else {
223                    // Remove the previous content if the new content is unknown.
224                    self.name.take().is_some()
225                }
226            }
227            // `m.room.create` CANNOT be overwritten.
228            (StateEventType::RoomCreate, "") if self.create.is_none() => {
229                if let Some(event) = raw_event.deserialize_as(|any_event| {
230                    as_variant!(any_event, AnySyncStateEvent::RoomCreate)
231                }) {
232                    self.create = Some(event.into());
233                    true
234                } else {
235                    false
236                }
237            }
238            (StateEventType::RoomHistoryVisibility, "") => {
239                if let Some(event) = raw_event.deserialize_as(|any_event| {
240                    as_variant!(any_event, AnySyncStateEvent::RoomHistoryVisibility)
241                }) {
242                    self.history_visibility = Some(event.into());
243                    true
244                } else {
245                    // Remove the previous content if the new content is unknown.
246                    self.history_visibility.take().is_some()
247                }
248            }
249            (StateEventType::RoomGuestAccess, "") => {
250                if let Some(event) = raw_event.deserialize_as(|any_event| {
251                    as_variant!(any_event, AnySyncStateEvent::RoomGuestAccess)
252                }) {
253                    self.guest_access = Some(event.into());
254                    true
255                } else {
256                    // Remove the previous content if the new content is unknown.
257                    self.guest_access.take().is_some()
258                }
259            }
260            (StateEventType::MemberHints, "") => {
261                if let Some(event) = raw_event.deserialize_as(|any_event| {
262                    as_variant!(any_event, AnySyncStateEvent::MemberHints)
263                }) {
264                    self.member_hints = Some(event.into());
265                    true
266                } else {
267                    // Remove the previous content if the new content is unknown.
268                    self.member_hints.take().is_some()
269                }
270            }
271            (StateEventType::RoomJoinRules, "") => {
272                if let Some(event) = raw_event.deserialize_as(|any_event| {
273                    as_variant!(any_event, AnySyncStateEvent::RoomJoinRules)
274                }) {
275                    match event.join_rule() {
276                        JoinRule::Invite
277                        | JoinRule::Knock
278                        | JoinRule::Private
279                        | JoinRule::Restricted(_)
280                        | JoinRule::KnockRestricted(_)
281                        | JoinRule::Public => {
282                            self.join_rules = Some(event.into());
283                            true
284                        }
285                        r => {
286                            warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
287                            // Remove the previous content if the new content is unsupported.
288                            self.join_rules.take().is_some()
289                        }
290                    }
291                } else {
292                    // Remove the previous content if the new content is unknown.
293                    self.join_rules.take().is_some()
294                }
295            }
296            (StateEventType::RoomCanonicalAlias, "") => {
297                if let Some(event) = raw_event.deserialize_as(|any_event| {
298                    as_variant!(any_event, AnySyncStateEvent::RoomCanonicalAlias)
299                }) {
300                    self.canonical_alias = Some(event.into());
301                    true
302                } else {
303                    // Remove the previous content if the new content is unknown.
304                    self.canonical_alias.take().is_some()
305                }
306            }
307            (StateEventType::RoomTopic, "") => {
308                if let Some(event) = raw_event.deserialize_as(|any_event| {
309                    as_variant!(any_event, AnySyncStateEvent::RoomTopic)
310                }) {
311                    self.topic = Some(event.into());
312                    true
313                } else {
314                    // Remove the previous content if the new content is unknown.
315                    self.topic.take().is_some()
316                }
317            }
318            (StateEventType::RoomTombstone, "") => {
319                if let Some(event) = raw_event.deserialize_as(|any_event| {
320                    as_variant!(any_event, AnySyncStateEvent::RoomTombstone)
321                }) {
322                    self.tombstone = Some(event.into());
323                    true
324                } else {
325                    // Remove the previous content if the new content is unknown.
326                    self.tombstone.take().is_some()
327                }
328            }
329            (StateEventType::RoomPowerLevels, "") => {
330                if let Some(event) = raw_event.deserialize_as(|any_event| {
331                    as_variant!(any_event, AnySyncStateEvent::RoomPowerLevels)
332                }) {
333                    // The rules and creators do not affect the max power level.
334                    self.max_power_level =
335                        event.power_levels(&AuthorizationRules::V1, vec![]).max().into();
336                    true
337                } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
338                    // Reset the previous value if the new value is unknown.
339                    self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
340                    true
341                } else {
342                    false
343                }
344            }
345            (StateEventType::CallMember, _) => {
346                if let Some(SyncStateEvent::Original(event)) =
347                    raw_event.deserialize_as(|any_event| {
348                        as_variant!(any_event, AnySyncStateEvent::CallMember)
349                    })
350                {
351                    // we modify the event so that `origin_server_ts` gets copied into
352                    // `content.created_ts`
353                    let mut event = event.clone();
354                    event.content.set_created_ts_if_none(event.origin_server_ts);
355
356                    // Add the new event.
357                    self.rtc_member_events
358                        .insert(event.state_key.clone(), SyncStateEvent::Original(event).into());
359
360                    // Remove all events that don't contain any memberships anymore.
361                    self.rtc_member_events
362                        .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
363
364                    true
365                } else if let Ok(call_member_key) =
366                    raw_event.state_key.parse::<CallMemberStateKey>()
367                {
368                    // Remove the previous content with the same state key if the new content is
369                    // unknown.
370                    self.rtc_member_events.remove(&call_member_key).is_some()
371                } else {
372                    false
373                }
374            }
375            (StateEventType::RoomPinnedEvents, "") => {
376                if let Some(SyncStateEvent::Original(event)) =
377                    raw_event.deserialize_as(|any_event| {
378                        as_variant!(any_event, AnySyncStateEvent::RoomPinnedEvents)
379                    })
380                {
381                    self.pinned_events = Some(event.content.clone());
382                    true
383                } else {
384                    // Remove the previous content if the new content is unknown.
385                    self.pinned_events.take().is_some()
386                }
387            }
388            _ => false,
389        }
390    }
391
392    /// Handle a stripped state event for this room and update our info
393    /// accordingly.
394    ///
395    /// Returns true if the event modified the info, false otherwise.
396    pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
397        match ev {
398            AnyStrippedStateEvent::RoomEncryption(encryption) => {
399                if let Some(algorithm) = &encryption.content.algorithm {
400                    let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
401                        rotation_period_ms: encryption.content.rotation_period_ms,
402                        rotation_period_msgs: encryption.content.rotation_period_msgs,
403                    });
404                    self.encryption = Some(content);
405                }
406                // If encryption event is redacted, we don't care much. When
407                // entering the room, we will fetch the proper event before
408                // sending any messages.
409            }
410            AnyStrippedStateEvent::RoomAvatar(a) => {
411                self.avatar = Some(a.into());
412            }
413            AnyStrippedStateEvent::RoomName(n) => {
414                self.name = Some(n.into());
415            }
416            AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
417                self.create = Some(c.into());
418            }
419            AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
420                self.history_visibility = Some(h.into());
421            }
422            AnyStrippedStateEvent::RoomGuestAccess(g) => {
423                self.guest_access = Some(g.into());
424            }
425            AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
426                JoinRule::Invite
427                | JoinRule::Knock
428                | JoinRule::Private
429                | JoinRule::Restricted(_)
430                | JoinRule::KnockRestricted(_)
431                | JoinRule::Public => self.join_rules = Some(c.into()),
432                r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
433            },
434            AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
435                self.canonical_alias = Some(a.into());
436            }
437            AnyStrippedStateEvent::RoomTopic(t) => {
438                self.topic = Some(t.into());
439            }
440            AnyStrippedStateEvent::RoomTombstone(t) => {
441                self.tombstone = Some(t.into());
442            }
443            AnyStrippedStateEvent::RoomPowerLevels(p) => {
444                // The rules and creators do not affect the max power level.
445                self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
446            }
447            AnyStrippedStateEvent::CallMember(_) => {
448                // Ignore stripped call state events. Rooms that are not in Joined or Left state
449                // wont have call information.
450                return false;
451            }
452            AnyStrippedStateEvent::RoomPinnedEvents(p) => {
453                if let Some(pinned) = p.content.pinned.clone() {
454                    self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
455                }
456            }
457            _ => return false,
458        }
459
460        true
461    }
462
463    pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
464        let redaction_rules = self
465            .room_version()
466            .and_then(|room_version| room_version.rules())
467            .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
468            .redaction;
469
470        if let Some(ev) = &mut self.avatar
471            && ev.event_id.as_deref() == Some(redacts)
472        {
473            ev.redact(&redaction_rules);
474        } else if let Some(ev) = &mut self.canonical_alias
475            && ev.event_id.as_deref() == Some(redacts)
476        {
477            ev.redact(&redaction_rules);
478        } else if let Some(ev) = &mut self.create
479            && ev.event_id.as_deref() == Some(redacts)
480        {
481            ev.redact(&redaction_rules);
482        } else if let Some(ev) = &mut self.guest_access
483            && ev.event_id.as_deref() == Some(redacts)
484        {
485            ev.redact(&redaction_rules);
486        } else if let Some(ev) = &mut self.history_visibility
487            && ev.event_id.as_deref() == Some(redacts)
488        {
489            ev.redact(&redaction_rules);
490        } else if let Some(ev) = &mut self.join_rules
491            && ev.event_id.as_deref() == Some(redacts)
492        {
493            ev.redact(&redaction_rules);
494        } else if let Some(ev) = &mut self.name
495            && ev.event_id.as_deref() == Some(redacts)
496        {
497            ev.redact(&redaction_rules);
498        } else if let Some(ev) = &mut self.tombstone
499            && ev.event_id.as_deref() == Some(redacts)
500        {
501            ev.redact(&redaction_rules);
502        } else if let Some(ev) = &mut self.topic
503            && ev.event_id.as_deref() == Some(redacts)
504        {
505            ev.redact(&redaction_rules);
506        } else {
507            self.rtc_member_events
508                .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
509        }
510    }
511
512    pub fn handle_notable_tags(&mut self, tags: &Tags) {
513        let mut notable_tags = RoomNotableTags::empty();
514
515        if tags.contains_key(&TagName::Favorite) {
516            notable_tags.insert(RoomNotableTags::FAVOURITE);
517        }
518
519        if tags.contains_key(&TagName::LowPriority) {
520            notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
521        }
522
523        self.notable_tags = notable_tags;
524    }
525}
526
527impl Default for BaseRoomInfo {
528    fn default() -> Self {
529        Self {
530            avatar: None,
531            canonical_alias: None,
532            create: None,
533            dm_targets: Default::default(),
534            member_hints: None,
535            encryption: None,
536            guest_access: None,
537            history_visibility: None,
538            join_rules: None,
539            max_power_level: DEFAULT_MAX_POWER_LEVEL,
540            name: None,
541            tombstone: None,
542            topic: None,
543            rtc_member_events: BTreeMap::new(),
544            is_marked_unread: false,
545            is_marked_unread_source: AccountDataSource::Unstable,
546            notable_tags: RoomNotableTags::empty(),
547            pinned_events: None,
548        }
549    }
550}
551
552/// The underlying pure data structure for joined and left rooms.
553///
554/// Holds all the info needed to persist a room into the state store.
555#[derive(Clone, Debug, Serialize, Deserialize)]
556pub struct RoomInfo {
557    /// The version of the room info type. It is used to migrate the `RoomInfo`
558    /// serialization from one version to another.
559    #[serde(default, alias = "version")]
560    pub(crate) data_format_version: u8,
561
562    /// The unique room id of the room.
563    pub(crate) room_id: OwnedRoomId,
564
565    /// The state of the room.
566    pub(crate) room_state: RoomState,
567
568    /// The unread notifications counts, as returned by the server.
569    ///
570    /// These might be incorrect for encrypted rooms, since the server doesn't
571    /// have access to the content of the encrypted events.
572    pub(crate) notification_counts: UnreadNotificationsCount,
573
574    /// The summary of this room.
575    pub(crate) summary: RoomSummary,
576
577    /// Flag remembering if the room members are synced.
578    pub(crate) members_synced: bool,
579
580    /// The prev batch of this room we received during the last sync.
581    pub(crate) last_prev_batch: Option<String>,
582
583    /// How much we know about this room.
584    pub(crate) sync_info: SyncInfo,
585
586    /// Whether or not the encryption info was been synced.
587    pub(crate) encryption_state_synced: bool,
588
589    /// The latest event value of this room.
590    #[serde(default)]
591    pub(crate) latest_event_value: LatestEventValue,
592
593    /// Information about read receipts for this room.
594    #[serde(default)]
595    pub(crate) read_receipts: RoomReadReceipts,
596
597    /// Base room info which holds some basic event contents important for the
598    /// room state.
599    pub(crate) base_info: Box<BaseRoomInfo>,
600
601    /// Whether we already warned about unknown room version rules in
602    /// [`RoomInfo::room_version_rules_or_default`]. This is done to avoid
603    /// spamming about unknown room versions rules in the log for the same room.
604    #[serde(skip)]
605    pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
606
607    /// Cached display name, useful for sync access.
608    ///
609    /// Filled by calling [`Room::compute_display_name`]. It's automatically
610    /// filled at start when creating a room, or on every successful sync.
611    #[serde(default, skip_serializing_if = "Option::is_none")]
612    pub(crate) cached_display_name: Option<RoomDisplayName>,
613
614    /// Cached user defined notification mode.
615    #[serde(default, skip_serializing_if = "Option::is_none")]
616    pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
617
618    /// The recency stamp of this room.
619    ///
620    /// It's not to be confused with the `origin_server_ts` value of an event.
621    /// Sliding Sync might “ignore” some events when computing the recency
622    /// stamp of the room. The recency stamp must be considered as an opaque
623    /// unsigned integer value.
624    ///
625    /// # Sorting rooms
626    ///
627    /// The recency stamp is designed to _sort_ rooms between them. The room
628    /// with the highest stamp should be at the top of a room list. However, in
629    /// some situation, it might be inaccurate (for example if the server and
630    /// the client disagree on which events should increment the recency stamp).
631    /// The [`LatestEventValue`] might be a useful alternative to sort rooms
632    /// between them as it's all computed client-side. In this case, the recency
633    /// stamp nicely acts as a default fallback.
634    #[serde(default)]
635    pub(crate) recency_stamp: Option<RoomRecencyStamp>,
636}
637
638impl RoomInfo {
639    #[doc(hidden)] // used by store tests, otherwise it would be pub(crate)
640    pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
641        Self {
642            data_format_version: 1,
643            room_id: room_id.into(),
644            room_state,
645            notification_counts: Default::default(),
646            summary: Default::default(),
647            members_synced: false,
648            last_prev_batch: None,
649            sync_info: SyncInfo::NoState,
650            encryption_state_synced: false,
651            latest_event_value: LatestEventValue::default(),
652            read_receipts: Default::default(),
653            base_info: Box::new(BaseRoomInfo::new()),
654            warned_about_unknown_room_version_rules: Arc::new(false.into()),
655            cached_display_name: None,
656            cached_user_defined_notification_mode: None,
657            recency_stamp: None,
658        }
659    }
660
661    /// Mark this Room as joined.
662    pub fn mark_as_joined(&mut self) {
663        self.set_state(RoomState::Joined);
664    }
665
666    /// Mark this Room as left.
667    pub fn mark_as_left(&mut self) {
668        self.set_state(RoomState::Left);
669    }
670
671    /// Mark this Room as invited.
672    pub fn mark_as_invited(&mut self) {
673        self.set_state(RoomState::Invited);
674    }
675
676    /// Mark this Room as knocked.
677    pub fn mark_as_knocked(&mut self) {
678        self.set_state(RoomState::Knocked);
679    }
680
681    /// Mark this Room as banned.
682    pub fn mark_as_banned(&mut self) {
683        self.set_state(RoomState::Banned);
684    }
685
686    /// Set the membership RoomState of this Room
687    pub fn set_state(&mut self, room_state: RoomState) {
688        self.room_state = room_state;
689    }
690
691    /// Mark this Room as having all the members synced.
692    pub fn mark_members_synced(&mut self) {
693        self.members_synced = true;
694    }
695
696    /// Mark this Room as still missing member information.
697    pub fn mark_members_missing(&mut self) {
698        self.members_synced = false;
699    }
700
701    /// Returns whether the room members are synced.
702    pub fn are_members_synced(&self) -> bool {
703        self.members_synced
704    }
705
706    /// Mark this Room as still missing some state information.
707    pub fn mark_state_partially_synced(&mut self) {
708        self.sync_info = SyncInfo::PartiallySynced;
709    }
710
711    /// Mark this Room as still having all state synced.
712    pub fn mark_state_fully_synced(&mut self) {
713        self.sync_info = SyncInfo::FullySynced;
714    }
715
716    /// Mark this Room as still having no state synced.
717    pub fn mark_state_not_synced(&mut self) {
718        self.sync_info = SyncInfo::NoState;
719    }
720
721    /// Mark this Room as having the encryption state synced.
722    pub fn mark_encryption_state_synced(&mut self) {
723        self.encryption_state_synced = true;
724    }
725
726    /// Mark this Room as still missing encryption state information.
727    pub fn mark_encryption_state_missing(&mut self) {
728        self.encryption_state_synced = false;
729    }
730
731    /// Set the `prev_batch`-token.
732    /// Returns whether the token has differed and thus has been upgraded:
733    /// `false` means no update was applied as the were the same
734    pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
735        if self.last_prev_batch.as_deref() != prev_batch {
736            self.last_prev_batch = prev_batch.map(|p| p.to_owned());
737            true
738        } else {
739            false
740        }
741    }
742
743    /// Returns the state this room is in.
744    pub fn state(&self) -> RoomState {
745        self.room_state
746    }
747
748    /// Returns the encryption state of this room.
749    #[cfg(not(feature = "experimental-encrypted-state-events"))]
750    pub fn encryption_state(&self) -> EncryptionState {
751        if !self.encryption_state_synced {
752            EncryptionState::Unknown
753        } else if self.base_info.encryption.is_some() {
754            EncryptionState::Encrypted
755        } else {
756            EncryptionState::NotEncrypted
757        }
758    }
759
760    /// Returns the encryption state of this room.
761    #[cfg(feature = "experimental-encrypted-state-events")]
762    pub fn encryption_state(&self) -> EncryptionState {
763        if !self.encryption_state_synced {
764            EncryptionState::Unknown
765        } else {
766            self.base_info
767                .encryption
768                .as_ref()
769                .map(|state| {
770                    if state.encrypt_state_events {
771                        EncryptionState::StateEncrypted
772                    } else {
773                        EncryptionState::Encrypted
774                    }
775                })
776                .unwrap_or(EncryptionState::NotEncrypted)
777        }
778    }
779
780    /// Set the encryption event content in this room.
781    pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
782        self.base_info.encryption = event;
783    }
784
785    /// Handle the encryption state.
786    pub fn handle_encryption_state(
787        &mut self,
788        requested_required_states: &[(StateEventType, String)],
789    ) {
790        if requested_required_states
791            .iter()
792            .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
793        {
794            // The `m.room.encryption` event was requested during the sync. Whether we have
795            // received a `m.room.encryption` event in return doesn't matter: we must mark
796            // the encryption state as synced; if the event is present, it means the room
797            // _is_ encrypted, otherwise it means the room _is not_ encrypted.
798
799            self.mark_encryption_state_synced();
800        }
801    }
802
803    /// Handle the given state event.
804    ///
805    /// Returns true if the event modified the info, false otherwise.
806    pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
807        // Store the state event in the `BaseRoomInfo` first.
808        let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
809
810        if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
811        {
812            // The `m.room.encryption` event was or wasn't explicitly requested, we don't
813            // know here (see `Self::handle_encryption_state`) but we got one in
814            // return! In this case, we can deduce the room _is_ encrypted, but we cannot
815            // know if it _is not_ encrypted.
816
817            self.mark_encryption_state_synced();
818        }
819
820        base_info_has_been_modified
821    }
822
823    /// Handle the given stripped state event.
824    ///
825    /// Returns true if the event modified the info, false otherwise.
826    pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
827        self.base_info.handle_stripped_state_event(event)
828    }
829
830    /// Handle the given redaction.
831    #[instrument(skip_all, fields(redacts))]
832    pub fn handle_redaction(
833        &mut self,
834        event: &SyncRoomRedactionEvent,
835        _raw: &Raw<SyncRoomRedactionEvent>,
836    ) {
837        let redaction_rules = self.room_version_rules_or_default().redaction;
838
839        let Some(redacts) = event.redacts(&redaction_rules) else {
840            info!("Can't apply redaction, redacts field is missing");
841            return;
842        };
843        tracing::Span::current().record("redacts", debug(redacts));
844
845        self.base_info.handle_redaction(redacts);
846    }
847
848    /// Returns the current room avatar.
849    pub fn avatar_url(&self) -> Option<&MxcUri> {
850        self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
851    }
852
853    /// Update the room avatar.
854    pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
855        self.base_info.avatar = url.map(|url| {
856            let mut content = PossiblyRedactedRoomAvatarEventContent::new();
857            content.url = Some(url);
858
859            MinimalStateEvent { content, event_id: None }
860        });
861    }
862
863    /// Returns information about the current room avatar.
864    pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
865        self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
866    }
867
868    /// Update the notifications count.
869    pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
870        self.notification_counts = notification_counts;
871    }
872
873    /// Update the RoomSummary from a Ruma `RoomSummary`.
874    ///
875    /// Returns true if any field has been updated, false otherwise.
876    pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
877        let mut changed = false;
878
879        if !summary.is_empty() {
880            if !summary.heroes.is_empty() {
881                self.summary.room_heroes = summary
882                    .heroes
883                    .iter()
884                    .map(|hero_id| RoomHero {
885                        user_id: hero_id.to_owned(),
886                        display_name: None,
887                        avatar_url: None,
888                    })
889                    .collect();
890
891                changed = true;
892            }
893
894            if let Some(joined) = summary.joined_member_count {
895                self.summary.joined_member_count = joined.into();
896                changed = true;
897            }
898
899            if let Some(invited) = summary.invited_member_count {
900                self.summary.invited_member_count = invited.into();
901                changed = true;
902            }
903        }
904
905        changed
906    }
907
908    /// Updates the joined member count.
909    pub(crate) fn update_joined_member_count(&mut self, count: u64) {
910        self.summary.joined_member_count = count;
911    }
912
913    /// Updates the invited member count.
914    pub(crate) fn update_invited_member_count(&mut self, count: u64) {
915        self.summary.invited_member_count = count;
916    }
917
918    /// Updates the room heroes.
919    pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
920        self.summary.room_heroes = heroes;
921    }
922
923    /// The heroes for this room.
924    pub fn heroes(&self) -> &[RoomHero] {
925        &self.summary.room_heroes
926    }
927
928    /// The number of active members (invited + joined) in the room.
929    ///
930    /// The return value is saturated at `u64::MAX`.
931    pub fn active_members_count(&self) -> u64 {
932        self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
933    }
934
935    /// The number of invited members in the room
936    pub fn invited_members_count(&self) -> u64 {
937        self.summary.invited_member_count
938    }
939
940    /// The number of joined members in the room
941    pub fn joined_members_count(&self) -> u64 {
942        self.summary.joined_member_count
943    }
944
945    /// Get the canonical alias of this room.
946    pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
947        self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
948    }
949
950    /// Get the alternative aliases of this room.
951    pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
952        self.base_info
953            .canonical_alias
954            .as_ref()
955            .map(|ev| ev.content.alt_aliases.as_ref())
956            .unwrap_or_default()
957    }
958
959    /// Get the room ID of this room.
960    pub fn room_id(&self) -> &RoomId {
961        &self.room_id
962    }
963
964    /// Get the room version of this room.
965    pub fn room_version(&self) -> Option<&RoomVersionId> {
966        self.base_info.room_version()
967    }
968
969    /// Get the room version rules of this room, or a sensible default.
970    ///
971    /// Will warn (at most once) if the room create event is missing from this
972    /// [`RoomInfo`] or if the room version is unsupported.
973    pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
974        use std::sync::atomic::Ordering;
975
976        self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
977            || {
978                if self
979                    .warned_about_unknown_room_version_rules
980                    .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
981                    .is_ok()
982                {
983                    warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
984                }
985
986                ROOM_VERSION_RULES_FALLBACK
987            },
988        )
989    }
990
991    /// Get the room type of this room.
992    pub fn room_type(&self) -> Option<&RoomType> {
993        self.base_info.create.as_ref()?.content.room_type.as_ref()
994    }
995
996    /// Get the creators of this room.
997    pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
998        Some(self.base_info.create.as_ref()?.content.creators())
999    }
1000
1001    pub(super) fn guest_access(&self) -> &GuestAccess {
1002        self.base_info
1003            .guest_access
1004            .as_ref()
1005            .and_then(|event| event.content.guest_access.as_ref())
1006            .unwrap_or(&GuestAccess::Forbidden)
1007    }
1008
1009    /// Returns the history visibility for this room.
1010    ///
1011    /// Returns None if the event was never seen during sync.
1012    pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1013        Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1014    }
1015
1016    /// Returns the history visibility for this room, or a sensible default.
1017    ///
1018    /// Returns `Shared`, the default specified by the [spec], when the event is
1019    /// missing.
1020    ///
1021    /// [spec]: https://spec.matrix.org/latest/client-server-api/#server-behaviour-7
1022    pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1023        self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1024    }
1025
1026    /// Return the join rule for this room, if the `m.room.join_rules` event is
1027    /// available.
1028    pub fn join_rule(&self) -> Option<&JoinRule> {
1029        Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1030    }
1031
1032    /// Return the service members for this room if the `m.member_hints` event
1033    /// is available
1034    pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1035        self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1036    }
1037
1038    /// Get the name of this room.
1039    pub fn name(&self) -> Option<&str> {
1040        self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1041    }
1042
1043    /// Get the content of the `m.room.create` event if any.
1044    pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1045        Some(&self.base_info.create.as_ref()?.content)
1046    }
1047
1048    /// Get the content of the `m.room.tombstone` event if any.
1049    pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1050        Some(&self.base_info.tombstone.as_ref()?.content)
1051    }
1052
1053    /// Returns the topic for this room, if set.
1054    pub fn topic(&self) -> Option<&str> {
1055        self.base_info.topic.as_ref()?.content.topic.as_deref()
1056    }
1057
1058    /// Get a list of all the valid (non expired) matrixRTC memberships and
1059    /// associated UserId's in this room.
1060    ///
1061    /// The vector is ordered by oldest membership to newest.
1062    fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1063        let mut v = self
1064            .base_info
1065            .rtc_member_events
1066            .iter()
1067            .flat_map(|(state_key, ev)| {
1068                ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1069            })
1070            .collect::<Vec<_>>();
1071        v.sort_by_key(|(_, m)| m.created_ts());
1072        v
1073    }
1074
1075    /// Similar to
1076    /// [`matrix_rtc_memberships`](Self::active_matrix_rtc_memberships) but only
1077    /// returns Memberships with application "m.call" and scope "m.room".
1078    ///
1079    /// The vector is ordered by oldest membership user to newest.
1080    fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1081        self.active_matrix_rtc_memberships()
1082            .into_iter()
1083            .filter(|(_user_id, m)| m.is_room_call())
1084            .collect()
1085    }
1086
1087    /// Is there a non expired membership with application "m.call" and scope
1088    /// "m.room" in this room.
1089    pub fn has_active_room_call(&self) -> bool {
1090        !self.active_room_call_memberships().is_empty()
1091    }
1092
1093    /// Returns a Vec of userId's that participate in the room call.
1094    ///
1095    /// matrix_rtc memberships with application "m.call" and scope "m.room" are
1096    /// considered. A user can occur twice if they join with two devices.
1097    /// convert to a set depending if the different users are required or the
1098    /// amount of sessions.
1099    ///
1100    /// The vector is ordered by oldest membership user to newest.
1101    pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1102        self.active_room_call_memberships()
1103            .iter()
1104            .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1105            .collect()
1106    }
1107
1108    /// Sets the new [`LatestEventValue`].
1109    pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1110        self.latest_event_value = new_value;
1111    }
1112
1113    /// Updates the recency stamp of this room.
1114    ///
1115    /// Please read `Self::recency_stamp` to learn more.
1116    pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1117        self.recency_stamp = Some(stamp);
1118    }
1119
1120    /// Returns the current pinned event ids for this room.
1121    pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1122        self.base_info.pinned_events.clone().map(|c| c.pinned)
1123    }
1124
1125    /// Checks if an `EventId` is currently pinned.
1126    /// It avoids having to clone the whole list of event ids to check a single
1127    /// value.
1128    ///
1129    /// Returns `true` if the provided `event_id` is pinned, `false` otherwise.
1130    pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1131        self.base_info
1132            .pinned_events
1133            .as_ref()
1134            .map(|p| p.pinned.contains(&event_id.to_owned()))
1135            .unwrap_or_default()
1136    }
1137
1138    /// Apply migrations to this `RoomInfo` if needed.
1139    ///
1140    /// This should be used to populate new fields with data from the state
1141    /// store.
1142    ///
1143    /// Returns `true` if migrations were applied and this `RoomInfo` needs to
1144    /// be persisted to the state store.
1145    #[instrument(skip_all, fields(room_id = ?self.room_id))]
1146    pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1147        let mut migrated = false;
1148
1149        if self.data_format_version < 1 {
1150            info!("Migrating room info to version 1");
1151
1152            // notable_tags
1153            match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1154                // Pinned events are never in stripped state.
1155                Ok(Some(raw_event)) => match raw_event.deserialize() {
1156                    Ok(event) => {
1157                        self.base_info.handle_notable_tags(&event.content.tags);
1158                    }
1159                    Err(error) => {
1160                        warn!("Failed to deserialize room tags: {error}");
1161                    }
1162                },
1163                Ok(_) => {
1164                    // Nothing to do.
1165                }
1166                Err(error) => {
1167                    warn!("Failed to load room tags: {error}");
1168                }
1169            }
1170
1171            // pinned_events
1172            match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1173            {
1174                // Pinned events are never in stripped state.
1175                Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1176                    if let Some(mut raw_event) =
1177                        RawSyncStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1178                    {
1179                        self.handle_state_event(&mut raw_event);
1180                    }
1181                }
1182                Ok(_) => {
1183                    // Nothing to do.
1184                }
1185                Err(error) => {
1186                    warn!("Failed to load room pinned events: {error}");
1187                }
1188            }
1189
1190            self.data_format_version = 1;
1191            migrated = true;
1192        }
1193
1194        migrated
1195    }
1196}
1197
1198/// Type to represent a `RoomInfo::recency_stamp`.
1199#[repr(transparent)]
1200#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1201#[serde(transparent)]
1202pub struct RoomRecencyStamp(u64);
1203
1204impl From<u64> for RoomRecencyStamp {
1205    fn from(value: u64) -> Self {
1206        Self(value)
1207    }
1208}
1209
1210impl From<RoomRecencyStamp> for u64 {
1211    fn from(value: RoomRecencyStamp) -> Self {
1212        value.0
1213    }
1214}
1215
1216#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1217pub(crate) enum SyncInfo {
1218    /// We only know the room exists and whether it is in invite / joined / left
1219    /// state.
1220    ///
1221    /// This is the case when we have a limited sync or only seen the room
1222    /// because of a request we've done, like a room creation event.
1223    NoState,
1224
1225    /// Some states have been synced, but they might have been filtered or is
1226    /// stale, as it is from a room we've left.
1227    PartiallySynced,
1228
1229    /// We have all the latest state events.
1230    FullySynced,
1231}
1232
1233/// Apply a redaction to the given target `event`, given the raw redaction event
1234/// and the room version.
1235pub fn apply_redaction(
1236    event: &Raw<AnySyncTimelineEvent>,
1237    raw_redaction: &Raw<SyncRoomRedactionEvent>,
1238    rules: &RedactionRules,
1239) -> Option<Raw<AnySyncTimelineEvent>> {
1240    use ruma::canonical_json::{RedactedBecause, redact_in_place};
1241
1242    let mut event_json = match event.deserialize_as() {
1243        Ok(json) => json,
1244        Err(e) => {
1245            warn!("Failed to deserialize latest event: {e}");
1246            return None;
1247        }
1248    };
1249
1250    let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1251        Ok(rb) => rb,
1252        Err(e) => {
1253            warn!("Redaction event is not valid canonical JSON: {e}");
1254            return None;
1255        }
1256    };
1257
1258    let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1259
1260    if let Err(e) = redact_result {
1261        warn!("Failed to redact event: {e}");
1262        return None;
1263    }
1264
1265    let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1266    Some(raw.cast_unchecked())
1267}
1268
1269/// Indicates that a notable update of `RoomInfo` has been applied, and why.
1270///
1271/// A room info notable update is an update that can be interesting for other
1272/// parts of the code. This mechanism is used in coordination with
1273/// [`BaseClient::room_info_notable_update_receiver`][baseclient] (and
1274/// `Room::info` plus `Room::room_info_notable_update_sender`) where `RoomInfo`
1275/// can be observed and some of its updates can be spread to listeners.
1276///
1277/// [baseclient]: crate::BaseClient::room_info_notable_update_receiver
1278#[derive(Debug, Clone)]
1279pub struct RoomInfoNotableUpdate {
1280    /// The room which was updated.
1281    pub room_id: OwnedRoomId,
1282
1283    /// The reason for this update.
1284    pub reasons: RoomInfoNotableUpdateReasons,
1285}
1286
1287bitflags! {
1288    /// The reason why a [`RoomInfoNotableUpdate`] is emitted.
1289    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1290    pub struct RoomInfoNotableUpdateReasons: u8 {
1291        /// The recency stamp of the `Room` has changed.
1292        const RECENCY_STAMP = 0b0000_0001;
1293
1294        /// The latest event of the `Room` has changed.
1295        const LATEST_EVENT = 0b0000_0010;
1296
1297        /// A read receipt has changed.
1298        const READ_RECEIPT = 0b0000_0100;
1299
1300        /// The user-controlled unread marker value has changed.
1301        const UNREAD_MARKER = 0b0000_1000;
1302
1303        /// A membership change happened for the current user.
1304        const MEMBERSHIP = 0b0001_0000;
1305
1306        /// The display name has changed.
1307        const DISPLAY_NAME = 0b0010_0000;
1308
1309        /// This is a temporary hack.
1310        ///
1311        /// So here is the thing. Ideally, we DO NOT want to emit this reason. It does not
1312        /// makes sense. However, all notable update reasons are not clearly identified
1313        /// so far. Why is it a problem? The `matrix_sdk_ui::room_list_service::RoomList`
1314        /// is listening this stream of [`RoomInfoNotableUpdate`], and emits an update on a
1315        /// room item if it receives a notable reason. Because all reasons are not
1316        /// identified, we are likely to miss particular updates, and it can feel broken.
1317        /// Ultimately, we want to clearly identify all the notable update reasons, and
1318        /// remove this one.
1319        const NONE = 0b1000_0000;
1320    }
1321}
1322
1323impl Default for RoomInfoNotableUpdateReasons {
1324    fn default() -> Self {
1325        Self::empty()
1326    }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331    use std::{str::FromStr, sync::Arc};
1332
1333    use assert_matches::assert_matches;
1334    use matrix_sdk_test::{async_test, event_factory::EventFactory};
1335    use ruma::{
1336        assign,
1337        events::{
1338            AnyRoomAccountDataEvent,
1339            room::pinned_events::RoomPinnedEventsEventContent,
1340            tag::{TagInfo, TagName, Tags, UserTagName},
1341        },
1342        owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1343        serde::Raw,
1344        user_id,
1345    };
1346    use serde_json::json;
1347    use similar_asserts::assert_eq;
1348
1349    use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1350    use crate::{
1351        RoomDisplayName, RoomHero, RoomState, StateChanges,
1352        notification_settings::RoomNotificationMode,
1353        room::{RoomNotableTags, RoomSummary},
1354        store::{IntoStateStore, MemoryStore},
1355        sync::UnreadNotificationsCount,
1356    };
1357
1358    #[test]
1359    fn test_room_info_serialization() {
1360        // This test exists to make sure we don't accidentally change the
1361        // serialized format for `RoomInfo`.
1362
1363        let info = RoomInfo {
1364            data_format_version: 1,
1365            room_id: room_id!("!gda78o:server.tld").into(),
1366            room_state: RoomState::Invited,
1367            notification_counts: UnreadNotificationsCount {
1368                highlight_count: 1,
1369                notification_count: 2,
1370            },
1371            summary: RoomSummary {
1372                room_heroes: vec![RoomHero {
1373                    user_id: owned_user_id!("@somebody:example.org"),
1374                    display_name: None,
1375                    avatar_url: None,
1376                }],
1377                joined_member_count: 5,
1378                invited_member_count: 0,
1379            },
1380            members_synced: true,
1381            last_prev_batch: Some("pb".to_owned()),
1382            sync_info: SyncInfo::FullySynced,
1383            encryption_state_synced: true,
1384            latest_event_value: LatestEventValue::None,
1385            base_info: Box::new(
1386                assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1387            ),
1388            read_receipts: Default::default(),
1389            warned_about_unknown_room_version_rules: Arc::new(false.into()),
1390            cached_display_name: None,
1391            cached_user_defined_notification_mode: None,
1392            recency_stamp: Some(42.into()),
1393        };
1394
1395        let info_json = json!({
1396            "data_format_version": 1,
1397            "room_id": "!gda78o:server.tld",
1398            "room_state": "Invited",
1399            "notification_counts": {
1400                "highlight_count": 1,
1401                "notification_count": 2,
1402            },
1403            "summary": {
1404                "room_heroes": [{
1405                    "user_id": "@somebody:example.org",
1406                    "display_name": null,
1407                    "avatar_url": null
1408                }],
1409                "joined_member_count": 5,
1410                "invited_member_count": 0,
1411            },
1412            "members_synced": true,
1413            "last_prev_batch": "pb",
1414            "sync_info": "FullySynced",
1415            "encryption_state_synced": true,
1416            "latest_event_value": "None",
1417            "base_info": {
1418                "avatar": null,
1419                "canonical_alias": null,
1420                "create": null,
1421                "dm_targets": [],
1422                "encryption": null,
1423                "guest_access": null,
1424                "history_visibility": null,
1425                "is_marked_unread": false,
1426                "is_marked_unread_source": "Unstable",
1427                "join_rules": null,
1428                "max_power_level": 100,
1429                "member_hints": null,
1430                "name": null,
1431                "tombstone": null,
1432                "topic": null,
1433                "pinned_events": {
1434                    "pinned": ["$a"]
1435                },
1436            },
1437            "read_receipts": {
1438                "num_unread": 0,
1439                "num_mentions": 0,
1440                "num_notifications": 0,
1441                "latest_active": null,
1442                "pending": [],
1443            },
1444            "recency_stamp": 42,
1445        });
1446
1447        assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1448    }
1449
1450    #[async_test]
1451    async fn test_room_info_migration_v1() {
1452        let store = MemoryStore::new().into_state_store();
1453
1454        let room_info_json = json!({
1455            "room_id": "!gda78o:server.tld",
1456            "room_state": "Joined",
1457            "notification_counts": {
1458                "highlight_count": 1,
1459                "notification_count": 2,
1460            },
1461            "summary": {
1462                "room_heroes": [{
1463                    "user_id": "@somebody:example.org",
1464                    "display_name": null,
1465                    "avatar_url": null
1466                }],
1467                "joined_member_count": 5,
1468                "invited_member_count": 0,
1469            },
1470            "members_synced": true,
1471            "last_prev_batch": "pb",
1472            "sync_info": "FullySynced",
1473            "encryption_state_synced": true,
1474            "latest_event": {
1475                "event": {
1476                    "encryption_info": null,
1477                    "event": {
1478                        "sender": "@u:i.uk",
1479                    },
1480                },
1481            },
1482            "base_info": {
1483                "avatar": null,
1484                "canonical_alias": null,
1485                "create": null,
1486                "dm_targets": [],
1487                "encryption": null,
1488                "guest_access": null,
1489                "history_visibility": null,
1490                "join_rules": null,
1491                "max_power_level": 100,
1492                "name": null,
1493                "tombstone": null,
1494                "topic": null,
1495            },
1496            "read_receipts": {
1497                "num_unread": 0,
1498                "num_mentions": 0,
1499                "num_notifications": 0,
1500                "latest_active": null,
1501                "pending": []
1502            },
1503            "recency_stamp": 42,
1504        });
1505        let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1506
1507        assert_eq!(room_info.data_format_version, 0);
1508        assert!(room_info.base_info.notable_tags.is_empty());
1509        assert!(room_info.base_info.pinned_events.is_none());
1510
1511        // Apply migrations with an empty store.
1512        assert!(room_info.apply_migrations(store.clone()).await);
1513
1514        assert_eq!(room_info.data_format_version, 1);
1515        assert!(room_info.base_info.notable_tags.is_empty());
1516        assert!(room_info.base_info.pinned_events.is_none());
1517
1518        // Applying migrations again has no effect.
1519        assert!(!room_info.apply_migrations(store.clone()).await);
1520
1521        assert_eq!(room_info.data_format_version, 1);
1522        assert!(room_info.base_info.notable_tags.is_empty());
1523        assert!(room_info.base_info.pinned_events.is_none());
1524
1525        // Add events to the store.
1526        let mut changes = StateChanges::default();
1527
1528        let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1529        let mut tags = Tags::new();
1530        tags.insert(TagName::Favorite, TagInfo::new());
1531        tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1532        let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1533        let tag_event = raw_tag_event.deserialize().unwrap();
1534        changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1535
1536        let raw_pinned_events_event: Raw<_> = f
1537            .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1538            .into_raw_sync_state();
1539        let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1540        changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1541
1542        store.save_changes(&changes).await.unwrap();
1543
1544        // Reset to version 0 and reapply migrations.
1545        room_info.data_format_version = 0;
1546        assert!(room_info.apply_migrations(store.clone()).await);
1547
1548        assert_eq!(room_info.data_format_version, 1);
1549        assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1550        assert!(room_info.base_info.pinned_events.is_some());
1551
1552        // Creating a new room info initializes it to version 1.
1553        let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1554        assert_eq!(new_room_info.data_format_version, 1);
1555    }
1556
1557    #[test]
1558    fn test_room_info_deserialization() {
1559        let info_json = json!({
1560            "room_id": "!gda78o:server.tld",
1561            "room_state": "Joined",
1562            "notification_counts": {
1563                "highlight_count": 1,
1564                "notification_count": 2,
1565            },
1566            "summary": {
1567                "room_heroes": [{
1568                    "user_id": "@somebody:example.org",
1569                    "display_name": "Somebody",
1570                    "avatar_url": "mxc://example.org/abc"
1571                }],
1572                "joined_member_count": 5,
1573                "invited_member_count": 0,
1574            },
1575            "members_synced": true,
1576            "last_prev_batch": "pb",
1577            "sync_info": "FullySynced",
1578            "encryption_state_synced": true,
1579            "base_info": {
1580                "avatar": null,
1581                "canonical_alias": null,
1582                "create": null,
1583                "dm_targets": [],
1584                "encryption": null,
1585                "guest_access": null,
1586                "history_visibility": null,
1587                "join_rules": null,
1588                "max_power_level": 100,
1589                "member_hints": null,
1590                "name": null,
1591                "tombstone": null,
1592                "topic": null,
1593            },
1594            "cached_display_name": { "Calculated": "lol" },
1595            "cached_user_defined_notification_mode": "Mute",
1596            "recency_stamp": 42,
1597        });
1598
1599        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1600
1601        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1602        assert_eq!(info.room_state, RoomState::Joined);
1603        assert_eq!(info.notification_counts.highlight_count, 1);
1604        assert_eq!(info.notification_counts.notification_count, 2);
1605        assert_eq!(
1606            info.summary.room_heroes,
1607            vec![RoomHero {
1608                user_id: owned_user_id!("@somebody:example.org"),
1609                display_name: Some("Somebody".to_owned()),
1610                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1611            }]
1612        );
1613        assert_eq!(info.summary.joined_member_count, 5);
1614        assert_eq!(info.summary.invited_member_count, 0);
1615        assert!(info.members_synced);
1616        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1617        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1618        assert!(info.encryption_state_synced);
1619        assert_matches!(info.latest_event_value, LatestEventValue::None);
1620        assert!(info.base_info.avatar.is_none());
1621        assert!(info.base_info.canonical_alias.is_none());
1622        assert!(info.base_info.create.is_none());
1623        assert_eq!(info.base_info.dm_targets.len(), 0);
1624        assert!(info.base_info.encryption.is_none());
1625        assert!(info.base_info.guest_access.is_none());
1626        assert!(info.base_info.history_visibility.is_none());
1627        assert!(info.base_info.join_rules.is_none());
1628        assert_eq!(info.base_info.max_power_level, 100);
1629        assert!(info.base_info.member_hints.is_none());
1630        assert!(info.base_info.name.is_none());
1631        assert!(info.base_info.tombstone.is_none());
1632        assert!(info.base_info.topic.is_none());
1633
1634        assert_eq!(
1635            info.cached_display_name.as_ref(),
1636            Some(&RoomDisplayName::Calculated("lol".to_owned())),
1637        );
1638        assert_eq!(
1639            info.cached_user_defined_notification_mode.as_ref(),
1640            Some(&RoomNotificationMode::Mute)
1641        );
1642        assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1643    }
1644
1645    // Ensure we can still deserialize RoomInfos before we added things to its
1646    // schema
1647    //
1648    // In an ideal world, we must not change this test. Please see
1649    // [`test_room_info_serialization`] if you want to test a “recent” `RoomInfo`
1650    // deserialization.
1651    #[test]
1652    fn test_room_info_deserialization_without_optional_items() {
1653        // The following JSON should never change if we want to be able to read in old
1654        // cached state
1655        let info_json = json!({
1656            "room_id": "!gda78o:server.tld",
1657            "room_state": "Invited",
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": "Somebody",
1666                    "avatar_url": "mxc://example.org/abc"
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            "base_info": {
1676                "avatar": null,
1677                "canonical_alias": null,
1678                "create": null,
1679                "dm_targets": [],
1680                "encryption": null,
1681                "guest_access": null,
1682                "history_visibility": null,
1683                "join_rules": null,
1684                "max_power_level": 100,
1685                "name": null,
1686                "tombstone": null,
1687                "topic": null,
1688            },
1689        });
1690
1691        let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1692
1693        assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1694        assert_eq!(info.room_state, RoomState::Invited);
1695        assert_eq!(info.notification_counts.highlight_count, 1);
1696        assert_eq!(info.notification_counts.notification_count, 2);
1697        assert_eq!(
1698            info.summary.room_heroes,
1699            vec![RoomHero {
1700                user_id: owned_user_id!("@somebody:example.org"),
1701                display_name: Some("Somebody".to_owned()),
1702                avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1703            }]
1704        );
1705        assert_eq!(info.summary.joined_member_count, 5);
1706        assert_eq!(info.summary.invited_member_count, 0);
1707        assert!(info.members_synced);
1708        assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1709        assert_eq!(info.sync_info, SyncInfo::FullySynced);
1710        assert!(info.encryption_state_synced);
1711        assert!(info.base_info.avatar.is_none());
1712        assert!(info.base_info.canonical_alias.is_none());
1713        assert!(info.base_info.create.is_none());
1714        assert_eq!(info.base_info.dm_targets.len(), 0);
1715        assert!(info.base_info.encryption.is_none());
1716        assert!(info.base_info.guest_access.is_none());
1717        assert!(info.base_info.history_visibility.is_none());
1718        assert!(info.base_info.join_rules.is_none());
1719        assert_eq!(info.base_info.max_power_level, 100);
1720        assert!(info.base_info.name.is_none());
1721        assert!(info.base_info.tombstone.is_none());
1722        assert!(info.base_info.topic.is_none());
1723    }
1724}