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