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