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