matrix_sdk_base/room/
room_info.rs

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