matrix_sdk_base/room/
room_info.rs

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