Skip to main content

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