1use 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 assign,
29 events::{
30 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
31 SyncStateEvent,
32 call::member::{
33 CallMemberStateKey, MembershipData, PossiblyRedactedCallMemberEventContent,
34 },
35 direct::OwnedDirectUserIdentifier,
36 member_hints::PossiblyRedactedMemberHintsEventContent,
37 room::{
38 avatar::{self, PossiblyRedactedRoomAvatarEventContent},
39 canonical_alias::PossiblyRedactedRoomCanonicalAliasEventContent,
40 encryption::RoomEncryptionEventContent,
41 guest_access::{GuestAccess, PossiblyRedactedRoomGuestAccessEventContent},
42 history_visibility::{
43 HistoryVisibility, PossiblyRedactedRoomHistoryVisibilityEventContent,
44 },
45 join_rules::{JoinRule, PossiblyRedactedRoomJoinRulesEventContent},
46 name::PossiblyRedactedRoomNameEventContent,
47 pinned_events::RoomPinnedEventsEventContent,
48 redaction::SyncRoomRedactionEvent,
49 tombstone::PossiblyRedactedRoomTombstoneEventContent,
50 topic::PossiblyRedactedRoomTopicEventContent,
51 },
52 tag::{TagEventContent, TagName, Tags},
53 },
54 room::RoomType,
55 room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
56 serde::Raw,
57};
58use serde::{Deserialize, Serialize};
59use tracing::{field::debug, info, instrument, warn};
60
61use super::{
62 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
63 RoomHero, RoomNotableTags, RoomState, RoomSummary,
64};
65use crate::{
66 MinimalStateEvent,
67 deserialized_responses::RawSyncOrStrippedState,
68 latest_event::LatestEventValue,
69 notification_settings::RoomNotificationMode,
70 read_receipts::RoomReadReceipts,
71 store::{DynStateStore, StateStoreExt},
72 sync::UnreadNotificationsCount,
73 utils::RawSyncStateEventWithKeys,
74};
75
76const DEFAULT_MAX_POWER_LEVEL: i64 = 100;
78
79impl Room {
80 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
82 self.info.subscribe()
83 }
84
85 pub fn clone_info(&self) -> RoomInfo {
87 self.info.get()
88 }
89
90 pub fn set_room_info(
92 &self,
93 room_info: RoomInfo,
94 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
95 ) {
96 self.info.set(room_info);
97
98 if !room_info_notable_update_reasons.is_empty() {
99 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
101 room_id: self.room_id.clone(),
102 reasons: room_info_notable_update_reasons,
103 });
104 } else {
105 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
109 room_id: self.room_id.clone(),
110 reasons: RoomInfoNotableUpdateReasons::NONE,
111 });
112 }
113 }
114}
115
116#[derive(Clone, Debug, Serialize, Deserialize)]
120pub struct BaseRoomInfo {
121 pub(crate) avatar: Option<MinimalStateEvent<PossiblyRedactedRoomAvatarEventContent>>,
123 pub(crate) canonical_alias:
125 Option<MinimalStateEvent<PossiblyRedactedRoomCanonicalAliasEventContent>>,
126 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
128 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
131 pub(crate) encryption: Option<RoomEncryptionEventContent>,
133 pub(crate) guest_access: Option<MinimalStateEvent<PossiblyRedactedRoomGuestAccessEventContent>>,
135 pub(crate) history_visibility:
137 Option<MinimalStateEvent<PossiblyRedactedRoomHistoryVisibilityEventContent>>,
138 pub(crate) join_rules: Option<MinimalStateEvent<PossiblyRedactedRoomJoinRulesEventContent>>,
140 pub(crate) max_power_level: i64,
142 pub(crate) member_hints: Option<MinimalStateEvent<PossiblyRedactedMemberHintsEventContent>>,
145 pub(crate) name: Option<MinimalStateEvent<PossiblyRedactedRoomNameEventContent>>,
147 pub(crate) tombstone: Option<MinimalStateEvent<PossiblyRedactedRoomTombstoneEventContent>>,
149 pub(crate) topic: Option<MinimalStateEvent<PossiblyRedactedRoomTopicEventContent>>,
151 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
154 pub(crate) rtc_member_events:
155 BTreeMap<CallMemberStateKey, MinimalStateEvent<PossiblyRedactedCallMemberEventContent>>,
156 #[serde(default)]
158 pub(crate) is_marked_unread: bool,
159 #[serde(default)]
161 pub(crate) is_marked_unread_source: AccountDataSource,
162 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
167 pub(crate) notable_tags: RoomNotableTags,
168 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
170}
171
172impl BaseRoomInfo {
173 pub fn new() -> Self {
175 Self::default()
176 }
177
178 pub fn room_version(&self) -> Option<&RoomVersionId> {
183 Some(&self.create.as_ref()?.content.room_version)
184 }
185
186 pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
190 match (&raw_event.event_type, raw_event.state_key.as_str()) {
191 (StateEventType::RoomEncryption, "") => {
192 if let Some(SyncStateEvent::Original(event)) =
195 raw_event.deserialize_as(|any_event| {
196 as_variant!(any_event, AnySyncStateEvent::RoomEncryption)
197 })
198 {
199 self.encryption = Some(event.content.clone());
200 true
201 } else {
202 false
203 }
204 }
205 (StateEventType::RoomAvatar, "") => {
206 if let Some(event) = raw_event.deserialize_as(|any_event| {
207 as_variant!(any_event, AnySyncStateEvent::RoomAvatar)
208 }) {
209 self.avatar = Some(event.into());
210 true
211 } else {
212 self.avatar.take().is_some()
214 }
215 }
216 (StateEventType::RoomName, "") => {
217 if let Some(event) = raw_event
218 .deserialize_as(|any_event| as_variant!(any_event, AnySyncStateEvent::RoomName))
219 {
220 self.name = Some(event.into());
221 true
222 } else {
223 self.name.take().is_some()
225 }
226 }
227 (StateEventType::RoomCreate, "") if self.create.is_none() => {
229 if let Some(event) = raw_event.deserialize_as(|any_event| {
230 as_variant!(any_event, AnySyncStateEvent::RoomCreate)
231 }) {
232 self.create = Some(event.into());
233 true
234 } else {
235 false
236 }
237 }
238 (StateEventType::RoomHistoryVisibility, "") => {
239 if let Some(event) = raw_event.deserialize_as(|any_event| {
240 as_variant!(any_event, AnySyncStateEvent::RoomHistoryVisibility)
241 }) {
242 self.history_visibility = Some(event.into());
243 true
244 } else {
245 self.history_visibility.take().is_some()
247 }
248 }
249 (StateEventType::RoomGuestAccess, "") => {
250 if let Some(event) = raw_event.deserialize_as(|any_event| {
251 as_variant!(any_event, AnySyncStateEvent::RoomGuestAccess)
252 }) {
253 self.guest_access = Some(event.into());
254 true
255 } else {
256 self.guest_access.take().is_some()
258 }
259 }
260 (StateEventType::MemberHints, "") => {
261 if let Some(event) = raw_event.deserialize_as(|any_event| {
262 as_variant!(any_event, AnySyncStateEvent::MemberHints)
263 }) {
264 self.member_hints = Some(event.into());
265 true
266 } else {
267 self.member_hints.take().is_some()
269 }
270 }
271 (StateEventType::RoomJoinRules, "") => {
272 if let Some(event) = raw_event.deserialize_as(|any_event| {
273 as_variant!(any_event, AnySyncStateEvent::RoomJoinRules)
274 }) {
275 match event.join_rule() {
276 JoinRule::Invite
277 | JoinRule::Knock
278 | JoinRule::Private
279 | JoinRule::Restricted(_)
280 | JoinRule::KnockRestricted(_)
281 | JoinRule::Public => {
282 self.join_rules = Some(event.into());
283 true
284 }
285 r => {
286 warn!(join_rule = ?r.as_str(), "Encountered a custom join rule, skipping");
287 self.join_rules.take().is_some()
289 }
290 }
291 } else {
292 self.join_rules.take().is_some()
294 }
295 }
296 (StateEventType::RoomCanonicalAlias, "") => {
297 if let Some(event) = raw_event.deserialize_as(|any_event| {
298 as_variant!(any_event, AnySyncStateEvent::RoomCanonicalAlias)
299 }) {
300 self.canonical_alias = Some(event.into());
301 true
302 } else {
303 self.canonical_alias.take().is_some()
305 }
306 }
307 (StateEventType::RoomTopic, "") => {
308 if let Some(event) = raw_event.deserialize_as(|any_event| {
309 as_variant!(any_event, AnySyncStateEvent::RoomTopic)
310 }) {
311 self.topic = Some(event.into());
312 true
313 } else {
314 self.topic.take().is_some()
316 }
317 }
318 (StateEventType::RoomTombstone, "") => {
319 if let Some(event) = raw_event.deserialize_as(|any_event| {
320 as_variant!(any_event, AnySyncStateEvent::RoomTombstone)
321 }) {
322 self.tombstone = Some(event.into());
323 true
324 } else {
325 self.tombstone.take().is_some()
327 }
328 }
329 (StateEventType::RoomPowerLevels, "") => {
330 if let Some(event) = raw_event.deserialize_as(|any_event| {
331 as_variant!(any_event, AnySyncStateEvent::RoomPowerLevels)
332 }) {
333 self.max_power_level =
335 event.power_levels(&AuthorizationRules::V1, vec![]).max().into();
336 true
337 } else if self.max_power_level != DEFAULT_MAX_POWER_LEVEL {
338 self.max_power_level = DEFAULT_MAX_POWER_LEVEL;
340 true
341 } else {
342 false
343 }
344 }
345 (StateEventType::CallMember, _) => {
346 if let Some(SyncStateEvent::Original(event)) =
347 raw_event.deserialize_as(|any_event| {
348 as_variant!(any_event, AnySyncStateEvent::CallMember)
349 })
350 {
351 let mut event = event.clone();
354 event.content.set_created_ts_if_none(event.origin_server_ts);
355
356 self.rtc_member_events
358 .insert(event.state_key.clone(), SyncStateEvent::Original(event).into());
359
360 self.rtc_member_events
362 .retain(|_, ev| !ev.content.active_memberships(None).is_empty());
363
364 true
365 } else if let Ok(call_member_key) =
366 raw_event.state_key.parse::<CallMemberStateKey>()
367 {
368 self.rtc_member_events.remove(&call_member_key).is_some()
371 } else {
372 false
373 }
374 }
375 (StateEventType::RoomPinnedEvents, "") => {
376 if let Some(SyncStateEvent::Original(event)) =
377 raw_event.deserialize_as(|any_event| {
378 as_variant!(any_event, AnySyncStateEvent::RoomPinnedEvents)
379 })
380 {
381 self.pinned_events = Some(event.content.clone());
382 true
383 } else {
384 self.pinned_events.take().is_some()
386 }
387 }
388 _ => false,
389 }
390 }
391
392 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
397 match ev {
398 AnyStrippedStateEvent::RoomEncryption(encryption) => {
399 if let Some(algorithm) = &encryption.content.algorithm {
400 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
401 rotation_period_ms: encryption.content.rotation_period_ms,
402 rotation_period_msgs: encryption.content.rotation_period_msgs,
403 });
404 self.encryption = Some(content);
405 }
406 }
410 AnyStrippedStateEvent::RoomAvatar(a) => {
411 self.avatar = Some(a.into());
412 }
413 AnyStrippedStateEvent::RoomName(n) => {
414 self.name = Some(n.into());
415 }
416 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
417 self.create = Some(c.into());
418 }
419 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
420 self.history_visibility = Some(h.into());
421 }
422 AnyStrippedStateEvent::RoomGuestAccess(g) => {
423 self.guest_access = Some(g.into());
424 }
425 AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
426 JoinRule::Invite
427 | JoinRule::Knock
428 | JoinRule::Private
429 | JoinRule::Restricted(_)
430 | JoinRule::KnockRestricted(_)
431 | JoinRule::Public => self.join_rules = Some(c.into()),
432 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
433 },
434 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
435 self.canonical_alias = Some(a.into());
436 }
437 AnyStrippedStateEvent::RoomTopic(t) => {
438 self.topic = Some(t.into());
439 }
440 AnyStrippedStateEvent::RoomTombstone(t) => {
441 self.tombstone = Some(t.into());
442 }
443 AnyStrippedStateEvent::RoomPowerLevels(p) => {
444 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
446 }
447 AnyStrippedStateEvent::CallMember(_) => {
448 return false;
451 }
452 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
453 if let Some(pinned) = p.content.pinned.clone() {
454 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
455 }
456 }
457 _ => return false,
458 }
459
460 true
461 }
462
463 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
464 let redaction_rules = self
465 .room_version()
466 .and_then(|room_version| room_version.rules())
467 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
468 .redaction;
469
470 if let Some(ev) = &mut self.avatar
471 && ev.event_id.as_deref() == Some(redacts)
472 {
473 ev.redact(&redaction_rules);
474 } else if let Some(ev) = &mut self.canonical_alias
475 && ev.event_id.as_deref() == Some(redacts)
476 {
477 ev.redact(&redaction_rules);
478 } else if let Some(ev) = &mut self.create
479 && ev.event_id.as_deref() == Some(redacts)
480 {
481 ev.redact(&redaction_rules);
482 } else if let Some(ev) = &mut self.guest_access
483 && ev.event_id.as_deref() == Some(redacts)
484 {
485 ev.redact(&redaction_rules);
486 } else if let Some(ev) = &mut self.history_visibility
487 && ev.event_id.as_deref() == Some(redacts)
488 {
489 ev.redact(&redaction_rules);
490 } else if let Some(ev) = &mut self.join_rules
491 && ev.event_id.as_deref() == Some(redacts)
492 {
493 ev.redact(&redaction_rules);
494 } else if let Some(ev) = &mut self.name
495 && ev.event_id.as_deref() == Some(redacts)
496 {
497 ev.redact(&redaction_rules);
498 } else if let Some(ev) = &mut self.tombstone
499 && ev.event_id.as_deref() == Some(redacts)
500 {
501 ev.redact(&redaction_rules);
502 } else if let Some(ev) = &mut self.topic
503 && ev.event_id.as_deref() == Some(redacts)
504 {
505 ev.redact(&redaction_rules);
506 } else {
507 self.rtc_member_events
508 .retain(|_, member_event| member_event.event_id.as_deref() != Some(redacts));
509 }
510 }
511
512 pub fn handle_notable_tags(&mut self, tags: &Tags) {
513 let mut notable_tags = RoomNotableTags::empty();
514
515 if tags.contains_key(&TagName::Favorite) {
516 notable_tags.insert(RoomNotableTags::FAVOURITE);
517 }
518
519 if tags.contains_key(&TagName::LowPriority) {
520 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
521 }
522
523 self.notable_tags = notable_tags;
524 }
525}
526
527impl Default for BaseRoomInfo {
528 fn default() -> Self {
529 Self {
530 avatar: None,
531 canonical_alias: None,
532 create: None,
533 dm_targets: Default::default(),
534 member_hints: None,
535 encryption: None,
536 guest_access: None,
537 history_visibility: None,
538 join_rules: None,
539 max_power_level: DEFAULT_MAX_POWER_LEVEL,
540 name: None,
541 tombstone: None,
542 topic: None,
543 rtc_member_events: BTreeMap::new(),
544 is_marked_unread: false,
545 is_marked_unread_source: AccountDataSource::Unstable,
546 notable_tags: RoomNotableTags::empty(),
547 pinned_events: None,
548 }
549 }
550}
551
552#[derive(Clone, Debug, Serialize, Deserialize)]
556pub struct RoomInfo {
557 #[serde(default, alias = "version")]
560 pub(crate) data_format_version: u8,
561
562 pub(crate) room_id: OwnedRoomId,
564
565 pub(crate) room_state: RoomState,
567
568 pub(crate) notification_counts: UnreadNotificationsCount,
573
574 pub(crate) summary: RoomSummary,
576
577 pub(crate) members_synced: bool,
579
580 pub(crate) last_prev_batch: Option<String>,
582
583 pub(crate) sync_info: SyncInfo,
585
586 pub(crate) encryption_state_synced: bool,
588
589 #[serde(default)]
591 pub(crate) latest_event_value: LatestEventValue,
592
593 #[serde(default)]
595 pub(crate) read_receipts: RoomReadReceipts,
596
597 pub(crate) base_info: Box<BaseRoomInfo>,
600
601 #[serde(skip)]
605 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
606
607 #[serde(default, skip_serializing_if = "Option::is_none")]
612 pub(crate) cached_display_name: Option<RoomDisplayName>,
613
614 #[serde(default, skip_serializing_if = "Option::is_none")]
616 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
617
618 #[serde(default)]
635 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
636}
637
638impl RoomInfo {
639 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
641 Self {
642 data_format_version: 1,
643 room_id: room_id.into(),
644 room_state,
645 notification_counts: Default::default(),
646 summary: Default::default(),
647 members_synced: false,
648 last_prev_batch: None,
649 sync_info: SyncInfo::NoState,
650 encryption_state_synced: false,
651 latest_event_value: LatestEventValue::default(),
652 read_receipts: Default::default(),
653 base_info: Box::new(BaseRoomInfo::new()),
654 warned_about_unknown_room_version_rules: Arc::new(false.into()),
655 cached_display_name: None,
656 cached_user_defined_notification_mode: None,
657 recency_stamp: None,
658 }
659 }
660
661 pub fn mark_as_joined(&mut self) {
663 self.set_state(RoomState::Joined);
664 }
665
666 pub fn mark_as_left(&mut self) {
668 self.set_state(RoomState::Left);
669 }
670
671 pub fn mark_as_invited(&mut self) {
673 self.set_state(RoomState::Invited);
674 }
675
676 pub fn mark_as_knocked(&mut self) {
678 self.set_state(RoomState::Knocked);
679 }
680
681 pub fn mark_as_banned(&mut self) {
683 self.set_state(RoomState::Banned);
684 }
685
686 pub fn set_state(&mut self, room_state: RoomState) {
688 self.room_state = room_state;
689 }
690
691 pub fn mark_members_synced(&mut self) {
693 self.members_synced = true;
694 }
695
696 pub fn mark_members_missing(&mut self) {
698 self.members_synced = false;
699 }
700
701 pub fn are_members_synced(&self) -> bool {
703 self.members_synced
704 }
705
706 pub fn mark_state_partially_synced(&mut self) {
708 self.sync_info = SyncInfo::PartiallySynced;
709 }
710
711 pub fn mark_state_fully_synced(&mut self) {
713 self.sync_info = SyncInfo::FullySynced;
714 }
715
716 pub fn mark_state_not_synced(&mut self) {
718 self.sync_info = SyncInfo::NoState;
719 }
720
721 pub fn mark_encryption_state_synced(&mut self) {
723 self.encryption_state_synced = true;
724 }
725
726 pub fn mark_encryption_state_missing(&mut self) {
728 self.encryption_state_synced = false;
729 }
730
731 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
735 if self.last_prev_batch.as_deref() != prev_batch {
736 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
737 true
738 } else {
739 false
740 }
741 }
742
743 pub fn state(&self) -> RoomState {
745 self.room_state
746 }
747
748 #[cfg(not(feature = "experimental-encrypted-state-events"))]
750 pub fn encryption_state(&self) -> EncryptionState {
751 if !self.encryption_state_synced {
752 EncryptionState::Unknown
753 } else if self.base_info.encryption.is_some() {
754 EncryptionState::Encrypted
755 } else {
756 EncryptionState::NotEncrypted
757 }
758 }
759
760 #[cfg(feature = "experimental-encrypted-state-events")]
762 pub fn encryption_state(&self) -> EncryptionState {
763 if !self.encryption_state_synced {
764 EncryptionState::Unknown
765 } else {
766 self.base_info
767 .encryption
768 .as_ref()
769 .map(|state| {
770 if state.encrypt_state_events {
771 EncryptionState::StateEncrypted
772 } else {
773 EncryptionState::Encrypted
774 }
775 })
776 .unwrap_or(EncryptionState::NotEncrypted)
777 }
778 }
779
780 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
782 self.base_info.encryption = event;
783 }
784
785 pub fn handle_encryption_state(
787 &mut self,
788 requested_required_states: &[(StateEventType, String)],
789 ) {
790 if requested_required_states
791 .iter()
792 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
793 {
794 self.mark_encryption_state_synced();
800 }
801 }
802
803 pub fn handle_state_event(&mut self, raw_event: &mut RawSyncStateEventWithKeys) -> bool {
807 let base_info_has_been_modified = self.base_info.handle_state_event(raw_event);
809
810 if raw_event.event_type == StateEventType::RoomEncryption && raw_event.state_key.is_empty()
811 {
812 self.mark_encryption_state_synced();
818 }
819
820 base_info_has_been_modified
821 }
822
823 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
827 self.base_info.handle_stripped_state_event(event)
828 }
829
830 #[instrument(skip_all, fields(redacts))]
832 pub fn handle_redaction(
833 &mut self,
834 event: &SyncRoomRedactionEvent,
835 _raw: &Raw<SyncRoomRedactionEvent>,
836 ) {
837 let redaction_rules = self.room_version_rules_or_default().redaction;
838
839 let Some(redacts) = event.redacts(&redaction_rules) else {
840 info!("Can't apply redaction, redacts field is missing");
841 return;
842 };
843 tracing::Span::current().record("redacts", debug(redacts));
844
845 self.base_info.handle_redaction(redacts);
846 }
847
848 pub fn avatar_url(&self) -> Option<&MxcUri> {
850 self.base_info.avatar.as_ref().and_then(|e| e.content.url.as_deref())
851 }
852
853 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
855 self.base_info.avatar = url.map(|url| {
856 let mut content = PossiblyRedactedRoomAvatarEventContent::new();
857 content.url = Some(url);
858
859 MinimalStateEvent { content, event_id: None }
860 });
861 }
862
863 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
865 self.base_info.avatar.as_ref().and_then(|e| e.content.info.as_deref())
866 }
867
868 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
870 self.notification_counts = notification_counts;
871 }
872
873 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
877 let mut changed = false;
878
879 if !summary.is_empty() {
880 if !summary.heroes.is_empty() {
881 self.summary.room_heroes = summary
882 .heroes
883 .iter()
884 .map(|hero_id| RoomHero {
885 user_id: hero_id.to_owned(),
886 display_name: None,
887 avatar_url: None,
888 })
889 .collect();
890
891 changed = true;
892 }
893
894 if let Some(joined) = summary.joined_member_count {
895 self.summary.joined_member_count = joined.into();
896 changed = true;
897 }
898
899 if let Some(invited) = summary.invited_member_count {
900 self.summary.invited_member_count = invited.into();
901 changed = true;
902 }
903 }
904
905 changed
906 }
907
908 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
910 self.summary.joined_member_count = count;
911 }
912
913 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
915 self.summary.invited_member_count = count;
916 }
917
918 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
920 self.summary.room_heroes = heroes;
921 }
922
923 pub fn heroes(&self) -> &[RoomHero] {
925 &self.summary.room_heroes
926 }
927
928 pub fn active_members_count(&self) -> u64 {
932 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
933 }
934
935 pub fn invited_members_count(&self) -> u64 {
937 self.summary.invited_member_count
938 }
939
940 pub fn joined_members_count(&self) -> u64 {
942 self.summary.joined_member_count
943 }
944
945 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
947 self.base_info.canonical_alias.as_ref()?.content.alias.as_deref()
948 }
949
950 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
952 self.base_info
953 .canonical_alias
954 .as_ref()
955 .map(|ev| ev.content.alt_aliases.as_ref())
956 .unwrap_or_default()
957 }
958
959 pub fn room_id(&self) -> &RoomId {
961 &self.room_id
962 }
963
964 pub fn room_version(&self) -> Option<&RoomVersionId> {
966 self.base_info.room_version()
967 }
968
969 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
974 use std::sync::atomic::Ordering;
975
976 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
977 || {
978 if self
979 .warned_about_unknown_room_version_rules
980 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
981 .is_ok()
982 {
983 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
984 }
985
986 ROOM_VERSION_RULES_FALLBACK
987 },
988 )
989 }
990
991 pub fn room_type(&self) -> Option<&RoomType> {
993 self.base_info.create.as_ref()?.content.room_type.as_ref()
994 }
995
996 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
998 Some(self.base_info.create.as_ref()?.content.creators())
999 }
1000
1001 pub(super) fn guest_access(&self) -> &GuestAccess {
1002 self.base_info
1003 .guest_access
1004 .as_ref()
1005 .and_then(|event| event.content.guest_access.as_ref())
1006 .unwrap_or(&GuestAccess::Forbidden)
1007 }
1008
1009 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
1013 Some(&self.base_info.history_visibility.as_ref()?.content.history_visibility)
1014 }
1015
1016 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
1023 self.history_visibility().unwrap_or(&HistoryVisibility::Shared)
1024 }
1025
1026 pub fn join_rule(&self) -> Option<&JoinRule> {
1029 Some(&self.base_info.join_rules.as_ref()?.content.join_rule)
1030 }
1031
1032 pub fn service_members(&self) -> Option<&BTreeSet<OwnedUserId>> {
1035 self.base_info.member_hints.as_ref()?.content.service_members.as_ref()
1036 }
1037
1038 pub fn name(&self) -> Option<&str> {
1040 self.base_info.name.as_ref()?.content.name.as_deref().filter(|name| !name.is_empty())
1041 }
1042
1043 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
1045 Some(&self.base_info.create.as_ref()?.content)
1046 }
1047
1048 pub fn tombstone(&self) -> Option<&PossiblyRedactedRoomTombstoneEventContent> {
1050 Some(&self.base_info.tombstone.as_ref()?.content)
1051 }
1052
1053 pub fn topic(&self) -> Option<&str> {
1055 self.base_info.topic.as_ref()?.content.topic.as_deref()
1056 }
1057
1058 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1063 let mut v = self
1064 .base_info
1065 .rtc_member_events
1066 .iter()
1067 .flat_map(|(state_key, ev)| {
1068 ev.content.active_memberships(None).into_iter().map(move |m| (state_key.clone(), m))
1069 })
1070 .collect::<Vec<_>>();
1071 v.sort_by_key(|(_, m)| m.created_ts());
1072 v
1073 }
1074
1075 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1081 self.active_matrix_rtc_memberships()
1082 .into_iter()
1083 .filter(|(_user_id, m)| m.is_room_call())
1084 .collect()
1085 }
1086
1087 pub fn has_active_room_call(&self) -> bool {
1090 !self.active_room_call_memberships().is_empty()
1091 }
1092
1093 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1102 self.active_room_call_memberships()
1103 .iter()
1104 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1105 .collect()
1106 }
1107
1108 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1110 self.latest_event_value = new_value;
1111 }
1112
1113 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1117 self.recency_stamp = Some(stamp);
1118 }
1119
1120 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1122 self.base_info.pinned_events.clone().map(|c| c.pinned)
1123 }
1124
1125 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1131 self.base_info
1132 .pinned_events
1133 .as_ref()
1134 .map(|p| p.pinned.contains(&event_id.to_owned()))
1135 .unwrap_or_default()
1136 }
1137
1138 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1146 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1147 let mut migrated = false;
1148
1149 if self.data_format_version < 1 {
1150 info!("Migrating room info to version 1");
1151
1152 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1154 Ok(Some(raw_event)) => match raw_event.deserialize() {
1156 Ok(event) => {
1157 self.base_info.handle_notable_tags(&event.content.tags);
1158 }
1159 Err(error) => {
1160 warn!("Failed to deserialize room tags: {error}");
1161 }
1162 },
1163 Ok(_) => {
1164 }
1166 Err(error) => {
1167 warn!("Failed to load room tags: {error}");
1168 }
1169 }
1170
1171 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1173 {
1174 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1176 if let Some(mut raw_event) =
1177 RawSyncStateEventWithKeys::try_from_raw_state_event(raw_event.cast())
1178 {
1179 self.handle_state_event(&mut raw_event);
1180 }
1181 }
1182 Ok(_) => {
1183 }
1185 Err(error) => {
1186 warn!("Failed to load room pinned events: {error}");
1187 }
1188 }
1189
1190 self.data_format_version = 1;
1191 migrated = true;
1192 }
1193
1194 migrated
1195 }
1196}
1197
1198#[repr(transparent)]
1200#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1201#[serde(transparent)]
1202pub struct RoomRecencyStamp(u64);
1203
1204impl From<u64> for RoomRecencyStamp {
1205 fn from(value: u64) -> Self {
1206 Self(value)
1207 }
1208}
1209
1210impl From<RoomRecencyStamp> for u64 {
1211 fn from(value: RoomRecencyStamp) -> Self {
1212 value.0
1213 }
1214}
1215
1216#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1217pub(crate) enum SyncInfo {
1218 NoState,
1224
1225 PartiallySynced,
1228
1229 FullySynced,
1231}
1232
1233pub fn apply_redaction(
1236 event: &Raw<AnySyncTimelineEvent>,
1237 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1238 rules: &RedactionRules,
1239) -> Option<Raw<AnySyncTimelineEvent>> {
1240 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1241
1242 let mut event_json = match event.deserialize_as() {
1243 Ok(json) => json,
1244 Err(e) => {
1245 warn!("Failed to deserialize latest event: {e}");
1246 return None;
1247 }
1248 };
1249
1250 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1251 Ok(rb) => rb,
1252 Err(e) => {
1253 warn!("Redaction event is not valid canonical JSON: {e}");
1254 return None;
1255 }
1256 };
1257
1258 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1259
1260 if let Err(e) = redact_result {
1261 warn!("Failed to redact event: {e}");
1262 return None;
1263 }
1264
1265 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1266 Some(raw.cast_unchecked())
1267}
1268
1269#[derive(Debug, Clone)]
1279pub struct RoomInfoNotableUpdate {
1280 pub room_id: OwnedRoomId,
1282
1283 pub reasons: RoomInfoNotableUpdateReasons,
1285}
1286
1287bitflags! {
1288 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1290 pub struct RoomInfoNotableUpdateReasons: u8 {
1291 const RECENCY_STAMP = 0b0000_0001;
1293
1294 const LATEST_EVENT = 0b0000_0010;
1296
1297 const READ_RECEIPT = 0b0000_0100;
1299
1300 const UNREAD_MARKER = 0b0000_1000;
1302
1303 const MEMBERSHIP = 0b0001_0000;
1305
1306 const DISPLAY_NAME = 0b0010_0000;
1308
1309 const NONE = 0b1000_0000;
1320 }
1321}
1322
1323impl Default for RoomInfoNotableUpdateReasons {
1324 fn default() -> Self {
1325 Self::empty()
1326 }
1327}
1328
1329#[cfg(test)]
1330mod tests {
1331 use std::{str::FromStr, sync::Arc};
1332
1333 use assert_matches::assert_matches;
1334 use matrix_sdk_test::{async_test, event_factory::EventFactory};
1335 use ruma::{
1336 assign,
1337 events::{
1338 AnyRoomAccountDataEvent,
1339 room::pinned_events::RoomPinnedEventsEventContent,
1340 tag::{TagInfo, TagName, Tags, UserTagName},
1341 },
1342 owned_event_id, owned_mxc_uri, owned_user_id, room_id,
1343 serde::Raw,
1344 user_id,
1345 };
1346 use serde_json::json;
1347 use similar_asserts::assert_eq;
1348
1349 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1350 use crate::{
1351 RoomDisplayName, RoomHero, RoomState, StateChanges,
1352 notification_settings::RoomNotificationMode,
1353 room::{RoomNotableTags, RoomSummary},
1354 store::{IntoStateStore, MemoryStore},
1355 sync::UnreadNotificationsCount,
1356 };
1357
1358 #[test]
1359 fn test_room_info_serialization() {
1360 let info = RoomInfo {
1364 data_format_version: 1,
1365 room_id: room_id!("!gda78o:server.tld").into(),
1366 room_state: RoomState::Invited,
1367 notification_counts: UnreadNotificationsCount {
1368 highlight_count: 1,
1369 notification_count: 2,
1370 },
1371 summary: RoomSummary {
1372 room_heroes: vec![RoomHero {
1373 user_id: owned_user_id!("@somebody:example.org"),
1374 display_name: None,
1375 avatar_url: None,
1376 }],
1377 joined_member_count: 5,
1378 invited_member_count: 0,
1379 },
1380 members_synced: true,
1381 last_prev_batch: Some("pb".to_owned()),
1382 sync_info: SyncInfo::FullySynced,
1383 encryption_state_synced: true,
1384 latest_event_value: LatestEventValue::None,
1385 base_info: Box::new(
1386 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1387 ),
1388 read_receipts: Default::default(),
1389 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1390 cached_display_name: None,
1391 cached_user_defined_notification_mode: None,
1392 recency_stamp: Some(42.into()),
1393 };
1394
1395 let info_json = json!({
1396 "data_format_version": 1,
1397 "room_id": "!gda78o:server.tld",
1398 "room_state": "Invited",
1399 "notification_counts": {
1400 "highlight_count": 1,
1401 "notification_count": 2,
1402 },
1403 "summary": {
1404 "room_heroes": [{
1405 "user_id": "@somebody:example.org",
1406 "display_name": null,
1407 "avatar_url": null
1408 }],
1409 "joined_member_count": 5,
1410 "invited_member_count": 0,
1411 },
1412 "members_synced": true,
1413 "last_prev_batch": "pb",
1414 "sync_info": "FullySynced",
1415 "encryption_state_synced": true,
1416 "latest_event_value": "None",
1417 "base_info": {
1418 "avatar": null,
1419 "canonical_alias": null,
1420 "create": null,
1421 "dm_targets": [],
1422 "encryption": null,
1423 "guest_access": null,
1424 "history_visibility": null,
1425 "is_marked_unread": false,
1426 "is_marked_unread_source": "Unstable",
1427 "join_rules": null,
1428 "max_power_level": 100,
1429 "member_hints": null,
1430 "name": null,
1431 "tombstone": null,
1432 "topic": null,
1433 "pinned_events": {
1434 "pinned": ["$a"]
1435 },
1436 },
1437 "read_receipts": {
1438 "num_unread": 0,
1439 "num_mentions": 0,
1440 "num_notifications": 0,
1441 "latest_active": null,
1442 "pending": [],
1443 },
1444 "recency_stamp": 42,
1445 });
1446
1447 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1448 }
1449
1450 #[async_test]
1451 async fn test_room_info_migration_v1() {
1452 let store = MemoryStore::new().into_state_store();
1453
1454 let room_info_json = json!({
1455 "room_id": "!gda78o:server.tld",
1456 "room_state": "Joined",
1457 "notification_counts": {
1458 "highlight_count": 1,
1459 "notification_count": 2,
1460 },
1461 "summary": {
1462 "room_heroes": [{
1463 "user_id": "@somebody:example.org",
1464 "display_name": null,
1465 "avatar_url": null
1466 }],
1467 "joined_member_count": 5,
1468 "invited_member_count": 0,
1469 },
1470 "members_synced": true,
1471 "last_prev_batch": "pb",
1472 "sync_info": "FullySynced",
1473 "encryption_state_synced": true,
1474 "latest_event": {
1475 "event": {
1476 "encryption_info": null,
1477 "event": {
1478 "sender": "@u:i.uk",
1479 },
1480 },
1481 },
1482 "base_info": {
1483 "avatar": null,
1484 "canonical_alias": null,
1485 "create": null,
1486 "dm_targets": [],
1487 "encryption": null,
1488 "guest_access": null,
1489 "history_visibility": null,
1490 "join_rules": null,
1491 "max_power_level": 100,
1492 "name": null,
1493 "tombstone": null,
1494 "topic": null,
1495 },
1496 "read_receipts": {
1497 "num_unread": 0,
1498 "num_mentions": 0,
1499 "num_notifications": 0,
1500 "latest_active": null,
1501 "pending": []
1502 },
1503 "recency_stamp": 42,
1504 });
1505 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1506
1507 assert_eq!(room_info.data_format_version, 0);
1508 assert!(room_info.base_info.notable_tags.is_empty());
1509 assert!(room_info.base_info.pinned_events.is_none());
1510
1511 assert!(room_info.apply_migrations(store.clone()).await);
1513
1514 assert_eq!(room_info.data_format_version, 1);
1515 assert!(room_info.base_info.notable_tags.is_empty());
1516 assert!(room_info.base_info.pinned_events.is_none());
1517
1518 assert!(!room_info.apply_migrations(store.clone()).await);
1520
1521 assert_eq!(room_info.data_format_version, 1);
1522 assert!(room_info.base_info.notable_tags.is_empty());
1523 assert!(room_info.base_info.pinned_events.is_none());
1524
1525 let mut changes = StateChanges::default();
1527
1528 let f = EventFactory::new().room(&room_info.room_id).sender(user_id!("@example:localhost"));
1529 let mut tags = Tags::new();
1530 tags.insert(TagName::Favorite, TagInfo::new());
1531 tags.insert(TagName::User(UserTagName::from_str("u.work").unwrap()), TagInfo::new());
1532 let raw_tag_event: Raw<AnyRoomAccountDataEvent> = f.tag(tags).into();
1533 let tag_event = raw_tag_event.deserialize().unwrap();
1534 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1535
1536 let raw_pinned_events_event: Raw<_> = f
1537 .room_pinned_events(vec![owned_event_id!("$a"), owned_event_id!("$b")])
1538 .into_raw_sync_state();
1539 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1540 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1541
1542 store.save_changes(&changes).await.unwrap();
1543
1544 room_info.data_format_version = 0;
1546 assert!(room_info.apply_migrations(store.clone()).await);
1547
1548 assert_eq!(room_info.data_format_version, 1);
1549 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1550 assert!(room_info.base_info.pinned_events.is_some());
1551
1552 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1554 assert_eq!(new_room_info.data_format_version, 1);
1555 }
1556
1557 #[test]
1558 fn test_room_info_deserialization() {
1559 let info_json = json!({
1560 "room_id": "!gda78o:server.tld",
1561 "room_state": "Joined",
1562 "notification_counts": {
1563 "highlight_count": 1,
1564 "notification_count": 2,
1565 },
1566 "summary": {
1567 "room_heroes": [{
1568 "user_id": "@somebody:example.org",
1569 "display_name": "Somebody",
1570 "avatar_url": "mxc://example.org/abc"
1571 }],
1572 "joined_member_count": 5,
1573 "invited_member_count": 0,
1574 },
1575 "members_synced": true,
1576 "last_prev_batch": "pb",
1577 "sync_info": "FullySynced",
1578 "encryption_state_synced": true,
1579 "base_info": {
1580 "avatar": null,
1581 "canonical_alias": null,
1582 "create": null,
1583 "dm_targets": [],
1584 "encryption": null,
1585 "guest_access": null,
1586 "history_visibility": null,
1587 "join_rules": null,
1588 "max_power_level": 100,
1589 "member_hints": null,
1590 "name": null,
1591 "tombstone": null,
1592 "topic": null,
1593 },
1594 "cached_display_name": { "Calculated": "lol" },
1595 "cached_user_defined_notification_mode": "Mute",
1596 "recency_stamp": 42,
1597 });
1598
1599 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1600
1601 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1602 assert_eq!(info.room_state, RoomState::Joined);
1603 assert_eq!(info.notification_counts.highlight_count, 1);
1604 assert_eq!(info.notification_counts.notification_count, 2);
1605 assert_eq!(
1606 info.summary.room_heroes,
1607 vec![RoomHero {
1608 user_id: owned_user_id!("@somebody:example.org"),
1609 display_name: Some("Somebody".to_owned()),
1610 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1611 }]
1612 );
1613 assert_eq!(info.summary.joined_member_count, 5);
1614 assert_eq!(info.summary.invited_member_count, 0);
1615 assert!(info.members_synced);
1616 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1617 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1618 assert!(info.encryption_state_synced);
1619 assert_matches!(info.latest_event_value, LatestEventValue::None);
1620 assert!(info.base_info.avatar.is_none());
1621 assert!(info.base_info.canonical_alias.is_none());
1622 assert!(info.base_info.create.is_none());
1623 assert_eq!(info.base_info.dm_targets.len(), 0);
1624 assert!(info.base_info.encryption.is_none());
1625 assert!(info.base_info.guest_access.is_none());
1626 assert!(info.base_info.history_visibility.is_none());
1627 assert!(info.base_info.join_rules.is_none());
1628 assert_eq!(info.base_info.max_power_level, 100);
1629 assert!(info.base_info.member_hints.is_none());
1630 assert!(info.base_info.name.is_none());
1631 assert!(info.base_info.tombstone.is_none());
1632 assert!(info.base_info.topic.is_none());
1633
1634 assert_eq!(
1635 info.cached_display_name.as_ref(),
1636 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1637 );
1638 assert_eq!(
1639 info.cached_user_defined_notification_mode.as_ref(),
1640 Some(&RoomNotificationMode::Mute)
1641 );
1642 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1643 }
1644
1645 #[test]
1652 fn test_room_info_deserialization_without_optional_items() {
1653 let info_json = json!({
1656 "room_id": "!gda78o:server.tld",
1657 "room_state": "Invited",
1658 "notification_counts": {
1659 "highlight_count": 1,
1660 "notification_count": 2,
1661 },
1662 "summary": {
1663 "room_heroes": [{
1664 "user_id": "@somebody:example.org",
1665 "display_name": "Somebody",
1666 "avatar_url": "mxc://example.org/abc"
1667 }],
1668 "joined_member_count": 5,
1669 "invited_member_count": 0,
1670 },
1671 "members_synced": true,
1672 "last_prev_batch": "pb",
1673 "sync_info": "FullySynced",
1674 "encryption_state_synced": true,
1675 "base_info": {
1676 "avatar": null,
1677 "canonical_alias": null,
1678 "create": null,
1679 "dm_targets": [],
1680 "encryption": null,
1681 "guest_access": null,
1682 "history_visibility": null,
1683 "join_rules": null,
1684 "max_power_level": 100,
1685 "name": null,
1686 "tombstone": null,
1687 "topic": null,
1688 },
1689 });
1690
1691 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1692
1693 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1694 assert_eq!(info.room_state, RoomState::Invited);
1695 assert_eq!(info.notification_counts.highlight_count, 1);
1696 assert_eq!(info.notification_counts.notification_count, 2);
1697 assert_eq!(
1698 info.summary.room_heroes,
1699 vec![RoomHero {
1700 user_id: owned_user_id!("@somebody:example.org"),
1701 display_name: Some("Somebody".to_owned()),
1702 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1703 }]
1704 );
1705 assert_eq!(info.summary.joined_member_count, 5);
1706 assert_eq!(info.summary.invited_member_count, 0);
1707 assert!(info.members_synced);
1708 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1709 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1710 assert!(info.encryption_state_synced);
1711 assert!(info.base_info.avatar.is_none());
1712 assert!(info.base_info.canonical_alias.is_none());
1713 assert!(info.base_info.create.is_none());
1714 assert_eq!(info.base_info.dm_targets.len(), 0);
1715 assert!(info.base_info.encryption.is_none());
1716 assert!(info.base_info.guest_access.is_none());
1717 assert!(info.base_info.history_visibility.is_none());
1718 assert!(info.base_info.join_rules.is_none());
1719 assert_eq!(info.base_info.max_power_level, 100);
1720 assert!(info.base_info.name.is_none());
1721 assert!(info.base_info.tombstone.is_none());
1722 assert!(info.base_info.topic.is_none());
1723 }
1724}