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