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