1use std::{
16 collections::{BTreeMap, HashSet},
17 sync::{Arc, atomic::AtomicBool},
18};
19
20use bitflags::bitflags;
21use eyeball::Subscriber;
22use matrix_sdk_common::{ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK};
23use ruma::{
24 EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
25 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
26 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
27 assign,
28 events::{
29 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
30 SyncStateEvent,
31 beacon_info::BeaconInfoEventContent,
32 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
33 direct::OwnedDirectUserIdentifier,
34 room::{
35 avatar::{self, RoomAvatarEventContent},
36 canonical_alias::RoomCanonicalAliasEventContent,
37 encryption::RoomEncryptionEventContent,
38 guest_access::{GuestAccess, RoomGuestAccessEventContent},
39 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
40 join_rules::{JoinRule, RoomJoinRulesEventContent},
41 name::RoomNameEventContent,
42 pinned_events::RoomPinnedEventsEventContent,
43 redaction::SyncRoomRedactionEvent,
44 tombstone::RoomTombstoneEventContent,
45 topic::RoomTopicEventContent,
46 },
47 tag::{TagEventContent, TagName, Tags},
48 },
49 room::RoomType,
50 room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
51 serde::Raw,
52};
53use serde::{Deserialize, Serialize};
54use tracing::{error, field::debug, info, instrument, warn};
55
56use super::{
57 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
58 RoomHero, RoomNotableTags, RoomState, RoomSummary,
59};
60use crate::{
61 MinimalStateEvent, OriginalMinimalStateEvent,
62 deserialized_responses::RawSyncOrStrippedState,
63 latest_event::LatestEventValue,
64 notification_settings::RoomNotificationMode,
65 read_receipts::RoomReadReceipts,
66 store::{DynStateStore, StateStoreExt},
67 sync::UnreadNotificationsCount,
68};
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct InviteAcceptanceDetails {
74 pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
77
78 pub inviter: OwnedUserId,
80}
81
82impl Room {
83 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
85 self.info.subscribe()
86 }
87
88 pub fn clone_info(&self) -> RoomInfo {
90 self.info.get()
91 }
92
93 pub fn set_room_info(
95 &self,
96 room_info: RoomInfo,
97 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
98 ) {
99 self.info.set(room_info);
100
101 if !room_info_notable_update_reasons.is_empty() {
102 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
104 room_id: self.room_id.clone(),
105 reasons: room_info_notable_update_reasons,
106 });
107 } else {
108 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
112 room_id: self.room_id.clone(),
113 reasons: RoomInfoNotableUpdateReasons::NONE,
114 });
115 }
116 }
117}
118
119#[derive(Clone, Debug, Serialize, Deserialize)]
123pub struct BaseRoomInfo {
124 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
126 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
128 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
129 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
131 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
133 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
136 pub(crate) encryption: Option<RoomEncryptionEventContent>,
138 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
140 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
142 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
144 pub(crate) max_power_level: i64,
146 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
148 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
150 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
152 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
155 pub(crate) rtc_member_events:
156 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
157 #[serde(default)]
159 pub(crate) is_marked_unread: bool,
160 #[serde(default)]
162 pub(crate) is_marked_unread_source: AccountDataSource,
163 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
168 pub(crate) notable_tags: RoomNotableTags,
169 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
171}
172
173impl BaseRoomInfo {
174 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn room_version(&self) -> Option<&RoomVersionId> {
184 match self.create.as_ref()? {
185 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
186 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
187 }
188 }
189
190 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
194 match ev {
195 AnySyncStateEvent::BeaconInfo(b) => {
196 self.beacons.insert(b.state_key().clone(), b.into());
197 }
198 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
200 self.encryption = Some(encryption.content.clone());
201 }
202 AnySyncStateEvent::RoomAvatar(a) => {
203 self.avatar = Some(a.into());
204 }
205 AnySyncStateEvent::RoomName(n) => {
206 self.name = Some(n.into());
207 }
208 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
210 self.create = Some(c.into());
211 }
212 AnySyncStateEvent::RoomHistoryVisibility(h) => {
213 self.history_visibility = Some(h.into());
214 }
215 AnySyncStateEvent::RoomGuestAccess(g) => {
216 self.guest_access = Some(g.into());
217 }
218 AnySyncStateEvent::RoomJoinRules(c) => match c.join_rule() {
219 JoinRule::Invite
220 | JoinRule::Knock
221 | JoinRule::Private
222 | JoinRule::Restricted(_)
223 | JoinRule::KnockRestricted(_)
224 | JoinRule::Public => self.join_rules = Some(c.into()),
225 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
226 },
227 AnySyncStateEvent::RoomCanonicalAlias(a) => {
228 self.canonical_alias = Some(a.into());
229 }
230 AnySyncStateEvent::RoomTopic(t) => {
231 self.topic = Some(t.into());
232 }
233 AnySyncStateEvent::RoomTombstone(t) => {
234 self.tombstone = Some(t.into());
235 }
236 AnySyncStateEvent::RoomPowerLevels(p) => {
237 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
239 }
240 AnySyncStateEvent::CallMember(m) => {
241 let Some(o_ev) = m.as_original() else {
242 return false;
243 };
244
245 let mut o_ev = o_ev.clone();
248 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
249
250 self.rtc_member_events
252 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
253
254 self.rtc_member_events.retain(|_, ev| {
256 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
257 });
258 }
259 AnySyncStateEvent::RoomPinnedEvents(p) => {
260 self.pinned_events = p.as_original().map(|p| p.content.clone());
261 }
262 _ => return false,
263 }
264
265 true
266 }
267
268 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
273 match ev {
274 AnyStrippedStateEvent::RoomEncryption(encryption) => {
275 if let Some(algorithm) = &encryption.content.algorithm {
276 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
277 rotation_period_ms: encryption.content.rotation_period_ms,
278 rotation_period_msgs: encryption.content.rotation_period_msgs,
279 });
280 self.encryption = Some(content);
281 }
282 }
286 AnyStrippedStateEvent::RoomAvatar(a) => {
287 self.avatar = Some(a.into());
288 }
289 AnyStrippedStateEvent::RoomName(n) => {
290 self.name = Some(n.into());
291 }
292 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
293 self.create = Some(c.into());
294 }
295 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
296 self.history_visibility = Some(h.into());
297 }
298 AnyStrippedStateEvent::RoomGuestAccess(g) => {
299 self.guest_access = Some(g.into());
300 }
301 AnyStrippedStateEvent::RoomJoinRules(c) => match &c.content.join_rule {
302 JoinRule::Invite
303 | JoinRule::Knock
304 | JoinRule::Private
305 | JoinRule::Restricted(_)
306 | JoinRule::KnockRestricted(_)
307 | JoinRule::Public => self.join_rules = Some(c.into()),
308 r => warn!("Encountered a custom join rule {}, skipping", r.as_str()),
309 },
310 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
311 self.canonical_alias = Some(a.into());
312 }
313 AnyStrippedStateEvent::RoomTopic(t) => {
314 self.topic = Some(t.into());
315 }
316 AnyStrippedStateEvent::RoomTombstone(t) => {
317 self.tombstone = Some(t.into());
318 }
319 AnyStrippedStateEvent::RoomPowerLevels(p) => {
320 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
322 }
323 AnyStrippedStateEvent::CallMember(_) => {
324 return false;
327 }
328 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
329 if let Some(pinned) = p.content.pinned.clone() {
330 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
331 }
332 }
333 _ => return false,
334 }
335
336 true
337 }
338
339 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
340 let redaction_rules = self
341 .room_version()
342 .and_then(|room_version| room_version.rules())
343 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
344 .redaction;
345
346 if let Some(ev) = &mut self.avatar
347 && ev.event_id() == Some(redacts)
348 {
349 ev.redact(&redaction_rules);
350 } else if let Some(ev) = &mut self.canonical_alias
351 && ev.event_id() == Some(redacts)
352 {
353 ev.redact(&redaction_rules);
354 } else if let Some(ev) = &mut self.create
355 && ev.event_id() == Some(redacts)
356 {
357 ev.redact(&redaction_rules);
358 } else if let Some(ev) = &mut self.guest_access
359 && ev.event_id() == Some(redacts)
360 {
361 ev.redact(&redaction_rules);
362 } else if let Some(ev) = &mut self.history_visibility
363 && ev.event_id() == Some(redacts)
364 {
365 ev.redact(&redaction_rules);
366 } else if let Some(ev) = &mut self.join_rules
367 && ev.event_id() == Some(redacts)
368 {
369 ev.redact(&redaction_rules);
370 } else if let Some(ev) = &mut self.name
371 && ev.event_id() == Some(redacts)
372 {
373 ev.redact(&redaction_rules);
374 } else if let Some(ev) = &mut self.tombstone
375 && ev.event_id() == Some(redacts)
376 {
377 ev.redact(&redaction_rules);
378 } else if let Some(ev) = &mut self.topic
379 && ev.event_id() == Some(redacts)
380 {
381 ev.redact(&redaction_rules);
382 } else {
383 self.rtc_member_events
384 .retain(|_, member_event| member_event.event_id() != Some(redacts));
385 }
386 }
387
388 pub fn handle_notable_tags(&mut self, tags: &Tags) {
389 let mut notable_tags = RoomNotableTags::empty();
390
391 if tags.contains_key(&TagName::Favorite) {
392 notable_tags.insert(RoomNotableTags::FAVOURITE);
393 }
394
395 if tags.contains_key(&TagName::LowPriority) {
396 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
397 }
398
399 self.notable_tags = notable_tags;
400 }
401}
402
403impl Default for BaseRoomInfo {
404 fn default() -> Self {
405 Self {
406 avatar: None,
407 beacons: BTreeMap::new(),
408 canonical_alias: None,
409 create: None,
410 dm_targets: Default::default(),
411 encryption: None,
412 guest_access: None,
413 history_visibility: None,
414 join_rules: None,
415 max_power_level: 100,
416 name: None,
417 tombstone: None,
418 topic: None,
419 rtc_member_events: BTreeMap::new(),
420 is_marked_unread: false,
421 is_marked_unread_source: AccountDataSource::Unstable,
422 notable_tags: RoomNotableTags::empty(),
423 pinned_events: None,
424 }
425 }
426}
427
428#[derive(Clone, Debug, Serialize, Deserialize)]
432pub struct RoomInfo {
433 #[serde(default, alias = "version")]
436 pub(crate) data_format_version: u8,
437
438 pub(crate) room_id: OwnedRoomId,
440
441 pub(crate) room_state: RoomState,
443
444 pub(crate) notification_counts: UnreadNotificationsCount,
449
450 pub(crate) summary: RoomSummary,
452
453 pub(crate) members_synced: bool,
455
456 pub(crate) last_prev_batch: Option<String>,
458
459 pub(crate) sync_info: SyncInfo,
461
462 pub(crate) encryption_state_synced: bool,
464
465 #[serde(default)]
467 pub(crate) latest_event_value: LatestEventValue,
468
469 #[serde(default)]
471 pub(crate) read_receipts: RoomReadReceipts,
472
473 pub(crate) base_info: Box<BaseRoomInfo>,
476
477 #[serde(skip)]
481 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
482
483 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub(crate) cached_display_name: Option<RoomDisplayName>,
489
490 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
493
494 #[serde(default)]
511 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
512
513 #[serde(default, skip_serializing_if = "Option::is_none")]
519 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
520}
521
522impl RoomInfo {
523 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
525 Self {
526 data_format_version: 1,
527 room_id: room_id.into(),
528 room_state,
529 notification_counts: Default::default(),
530 summary: Default::default(),
531 members_synced: false,
532 last_prev_batch: None,
533 sync_info: SyncInfo::NoState,
534 encryption_state_synced: false,
535 latest_event_value: LatestEventValue::default(),
536 read_receipts: Default::default(),
537 base_info: Box::new(BaseRoomInfo::new()),
538 warned_about_unknown_room_version_rules: Arc::new(false.into()),
539 cached_display_name: None,
540 cached_user_defined_notification_mode: None,
541 recency_stamp: None,
542 invite_acceptance_details: None,
543 }
544 }
545
546 pub fn mark_as_joined(&mut self) {
548 self.set_state(RoomState::Joined);
549 }
550
551 pub fn mark_as_left(&mut self) {
553 self.set_state(RoomState::Left);
554 }
555
556 pub fn mark_as_invited(&mut self) {
558 self.set_state(RoomState::Invited);
559 }
560
561 pub fn mark_as_knocked(&mut self) {
563 self.set_state(RoomState::Knocked);
564 }
565
566 pub fn mark_as_banned(&mut self) {
568 self.set_state(RoomState::Banned);
569 }
570
571 pub fn set_state(&mut self, room_state: RoomState) {
573 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
574 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
575 }
576 self.invite_acceptance_details = None;
579 self.room_state = room_state;
580 }
581
582 pub fn mark_members_synced(&mut self) {
584 self.members_synced = true;
585 }
586
587 pub fn mark_members_missing(&mut self) {
589 self.members_synced = false;
590 }
591
592 pub fn are_members_synced(&self) -> bool {
594 self.members_synced
595 }
596
597 pub fn mark_state_partially_synced(&mut self) {
599 self.sync_info = SyncInfo::PartiallySynced;
600 }
601
602 pub fn mark_state_fully_synced(&mut self) {
604 self.sync_info = SyncInfo::FullySynced;
605 }
606
607 pub fn mark_state_not_synced(&mut self) {
609 self.sync_info = SyncInfo::NoState;
610 }
611
612 pub fn mark_encryption_state_synced(&mut self) {
614 self.encryption_state_synced = true;
615 }
616
617 pub fn mark_encryption_state_missing(&mut self) {
619 self.encryption_state_synced = false;
620 }
621
622 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
626 if self.last_prev_batch.as_deref() != prev_batch {
627 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
628 true
629 } else {
630 false
631 }
632 }
633
634 pub fn state(&self) -> RoomState {
636 self.room_state
637 }
638
639 #[cfg(not(feature = "experimental-encrypted-state-events"))]
641 pub fn encryption_state(&self) -> EncryptionState {
642 if !self.encryption_state_synced {
643 EncryptionState::Unknown
644 } else if self.base_info.encryption.is_some() {
645 EncryptionState::Encrypted
646 } else {
647 EncryptionState::NotEncrypted
648 }
649 }
650
651 #[cfg(feature = "experimental-encrypted-state-events")]
653 pub fn encryption_state(&self) -> EncryptionState {
654 if !self.encryption_state_synced {
655 EncryptionState::Unknown
656 } else {
657 self.base_info
658 .encryption
659 .as_ref()
660 .map(|state| {
661 if state.encrypt_state_events {
662 EncryptionState::StateEncrypted
663 } else {
664 EncryptionState::Encrypted
665 }
666 })
667 .unwrap_or(EncryptionState::NotEncrypted)
668 }
669 }
670
671 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
673 self.base_info.encryption = event;
674 }
675
676 pub fn handle_encryption_state(
678 &mut self,
679 requested_required_states: &[(StateEventType, String)],
680 ) {
681 if requested_required_states
682 .iter()
683 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
684 {
685 self.mark_encryption_state_synced();
691 }
692 }
693
694 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
698 let base_info_has_been_modified = self.base_info.handle_state_event(event);
700
701 if let AnySyncStateEvent::RoomEncryption(_) = event {
702 self.mark_encryption_state_synced();
708 }
709
710 base_info_has_been_modified
711 }
712
713 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
717 self.base_info.handle_stripped_state_event(event)
718 }
719
720 #[instrument(skip_all, fields(redacts))]
722 pub fn handle_redaction(
723 &mut self,
724 event: &SyncRoomRedactionEvent,
725 _raw: &Raw<SyncRoomRedactionEvent>,
726 ) {
727 let redaction_rules = self.room_version_rules_or_default().redaction;
728
729 let Some(redacts) = event.redacts(&redaction_rules) else {
730 info!("Can't apply redaction, redacts field is missing");
731 return;
732 };
733 tracing::Span::current().record("redacts", debug(redacts));
734
735 self.base_info.handle_redaction(redacts);
736 }
737
738 pub fn avatar_url(&self) -> Option<&MxcUri> {
740 self.base_info
741 .avatar
742 .as_ref()
743 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
744 }
745
746 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
748 self.base_info.avatar = url.map(|url| {
749 let mut content = RoomAvatarEventContent::new();
750 content.url = Some(url);
751
752 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
753 });
754 }
755
756 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
758 self.base_info
759 .avatar
760 .as_ref()
761 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
762 }
763
764 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
766 self.notification_counts = notification_counts;
767 }
768
769 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
773 let mut changed = false;
774
775 if !summary.is_empty() {
776 if !summary.heroes.is_empty() {
777 self.summary.room_heroes = summary
778 .heroes
779 .iter()
780 .map(|hero_id| RoomHero {
781 user_id: hero_id.to_owned(),
782 display_name: None,
783 avatar_url: None,
784 })
785 .collect();
786
787 changed = true;
788 }
789
790 if let Some(joined) = summary.joined_member_count {
791 self.summary.joined_member_count = joined.into();
792 changed = true;
793 }
794
795 if let Some(invited) = summary.invited_member_count {
796 self.summary.invited_member_count = invited.into();
797 changed = true;
798 }
799 }
800
801 changed
802 }
803
804 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
806 self.summary.joined_member_count = count;
807 }
808
809 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
811 self.summary.invited_member_count = count;
812 }
813
814 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
815 self.invite_acceptance_details = Some(details);
816 }
817
818 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
825 self.invite_acceptance_details.clone()
826 }
827
828 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
830 self.summary.room_heroes = heroes;
831 }
832
833 pub fn heroes(&self) -> &[RoomHero] {
835 &self.summary.room_heroes
836 }
837
838 pub fn active_members_count(&self) -> u64 {
842 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
843 }
844
845 pub fn invited_members_count(&self) -> u64 {
847 self.summary.invited_member_count
848 }
849
850 pub fn joined_members_count(&self) -> u64 {
852 self.summary.joined_member_count
853 }
854
855 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
857 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
858 }
859
860 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
862 self.base_info
863 .canonical_alias
864 .as_ref()
865 .and_then(|ev| ev.as_original())
866 .map(|ev| ev.content.alt_aliases.as_ref())
867 .unwrap_or_default()
868 }
869
870 pub fn room_id(&self) -> &RoomId {
872 &self.room_id
873 }
874
875 pub fn room_version(&self) -> Option<&RoomVersionId> {
877 self.base_info.room_version()
878 }
879
880 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
885 use std::sync::atomic::Ordering;
886
887 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
888 || {
889 if self
890 .warned_about_unknown_room_version_rules
891 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
892 .is_ok()
893 {
894 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
895 }
896
897 ROOM_VERSION_RULES_FALLBACK
898 },
899 )
900 }
901
902 pub fn room_type(&self) -> Option<&RoomType> {
904 match self.base_info.create.as_ref()? {
905 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
906 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
907 }
908 }
909
910 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
912 match self.base_info.create.as_ref()? {
913 MinimalStateEvent::Original(ev) => Some(ev.content.creators()),
914 MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()),
915 }
916 }
917
918 pub(super) fn guest_access(&self) -> &GuestAccess {
919 match &self.base_info.guest_access {
920 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
921 _ => &GuestAccess::Forbidden,
922 }
923 }
924
925 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
929 match &self.base_info.history_visibility {
930 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
931 _ => None,
932 }
933 }
934
935 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
942 match &self.base_info.history_visibility {
943 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
944 _ => &HistoryVisibility::Shared,
945 }
946 }
947
948 pub fn join_rule(&self) -> Option<&JoinRule> {
951 match &self.base_info.join_rules {
952 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
953 _ => None,
954 }
955 }
956
957 pub fn name(&self) -> Option<&str> {
959 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
960 (!name.is_empty()).then_some(name)
961 }
962
963 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
965 Some(&self.base_info.create.as_ref()?.as_original()?.content)
966 }
967
968 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
970 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
971 }
972
973 pub fn topic(&self) -> Option<&str> {
975 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
976 }
977
978 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
983 let mut v = self
984 .base_info
985 .rtc_member_events
986 .iter()
987 .filter_map(|(user_id, ev)| {
988 ev.as_original().map(|ev| {
989 ev.content
990 .active_memberships(None)
991 .into_iter()
992 .map(move |m| (user_id.clone(), m))
993 })
994 })
995 .flatten()
996 .collect::<Vec<_>>();
997 v.sort_by_key(|(_, m)| m.created_ts());
998 v
999 }
1000
1001 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1007 self.active_matrix_rtc_memberships()
1008 .into_iter()
1009 .filter(|(_user_id, m)| m.is_room_call())
1010 .collect()
1011 }
1012
1013 pub fn has_active_room_call(&self) -> bool {
1016 !self.active_room_call_memberships().is_empty()
1017 }
1018
1019 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1028 self.active_room_call_memberships()
1029 .iter()
1030 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1031 .collect()
1032 }
1033
1034 pub fn set_latest_event(&mut self, new_value: LatestEventValue) {
1036 self.latest_event_value = new_value;
1037 }
1038
1039 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1043 self.recency_stamp = Some(stamp);
1044 }
1045
1046 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1048 self.base_info.pinned_events.clone().map(|c| c.pinned)
1049 }
1050
1051 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1057 self.base_info
1058 .pinned_events
1059 .as_ref()
1060 .map(|p| p.pinned.contains(&event_id.to_owned()))
1061 .unwrap_or_default()
1062 }
1063
1064 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1072 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1073 let mut migrated = false;
1074
1075 if self.data_format_version < 1 {
1076 info!("Migrating room info to version 1");
1077
1078 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1080 Ok(Some(raw_event)) => match raw_event.deserialize() {
1082 Ok(event) => {
1083 self.base_info.handle_notable_tags(&event.content.tags);
1084 }
1085 Err(error) => {
1086 warn!("Failed to deserialize room tags: {error}");
1087 }
1088 },
1089 Ok(_) => {
1090 }
1092 Err(error) => {
1093 warn!("Failed to load room tags: {error}");
1094 }
1095 }
1096
1097 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1099 {
1100 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1102 match raw_event.deserialize() {
1103 Ok(event) => {
1104 self.handle_state_event(&event.into());
1105 }
1106 Err(error) => {
1107 warn!("Failed to deserialize room pinned events: {error}");
1108 }
1109 }
1110 }
1111 Ok(_) => {
1112 }
1114 Err(error) => {
1115 warn!("Failed to load room pinned events: {error}");
1116 }
1117 }
1118
1119 self.data_format_version = 1;
1120 migrated = true;
1121 }
1122
1123 migrated
1124 }
1125}
1126
1127#[repr(transparent)]
1129#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1130#[serde(transparent)]
1131pub struct RoomRecencyStamp(u64);
1132
1133impl From<u64> for RoomRecencyStamp {
1134 fn from(value: u64) -> Self {
1135 Self(value)
1136 }
1137}
1138
1139impl From<RoomRecencyStamp> for u64 {
1140 fn from(value: RoomRecencyStamp) -> Self {
1141 value.0
1142 }
1143}
1144
1145#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1146pub(crate) enum SyncInfo {
1147 NoState,
1153
1154 PartiallySynced,
1157
1158 FullySynced,
1160}
1161
1162pub fn apply_redaction(
1165 event: &Raw<AnySyncTimelineEvent>,
1166 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1167 rules: &RedactionRules,
1168) -> Option<Raw<AnySyncTimelineEvent>> {
1169 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1170
1171 let mut event_json = match event.deserialize_as() {
1172 Ok(json) => json,
1173 Err(e) => {
1174 warn!("Failed to deserialize latest event: {e}");
1175 return None;
1176 }
1177 };
1178
1179 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1180 Ok(rb) => rb,
1181 Err(e) => {
1182 warn!("Redaction event is not valid canonical JSON: {e}");
1183 return None;
1184 }
1185 };
1186
1187 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1188
1189 if let Err(e) = redact_result {
1190 warn!("Failed to redact event: {e}");
1191 return None;
1192 }
1193
1194 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1195 Some(raw.cast_unchecked())
1196}
1197
1198#[derive(Debug, Clone)]
1208pub struct RoomInfoNotableUpdate {
1209 pub room_id: OwnedRoomId,
1211
1212 pub reasons: RoomInfoNotableUpdateReasons,
1214}
1215
1216bitflags! {
1217 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1219 pub struct RoomInfoNotableUpdateReasons: u8 {
1220 const RECENCY_STAMP = 0b0000_0001;
1222
1223 const LATEST_EVENT = 0b0000_0010;
1225
1226 const READ_RECEIPT = 0b0000_0100;
1228
1229 const UNREAD_MARKER = 0b0000_1000;
1231
1232 const MEMBERSHIP = 0b0001_0000;
1234
1235 const DISPLAY_NAME = 0b0010_0000;
1237
1238 const NONE = 0b1000_0000;
1249 }
1250}
1251
1252impl Default for RoomInfoNotableUpdateReasons {
1253 fn default() -> Self {
1254 Self::empty()
1255 }
1256}
1257
1258#[cfg(test)]
1259mod tests {
1260 use std::sync::Arc;
1261
1262 use assert_matches::assert_matches;
1263 use matrix_sdk_test::{
1264 async_test,
1265 test_json::{TAG, sync_events::PINNED_EVENTS},
1266 };
1267 use ruma::{
1268 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1269 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1270 };
1271 use serde_json::json;
1272 use similar_asserts::assert_eq;
1273
1274 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1275 use crate::{
1276 RoomDisplayName, RoomHero, RoomState, StateChanges,
1277 notification_settings::RoomNotificationMode,
1278 room::{RoomNotableTags, RoomSummary},
1279 store::{IntoStateStore, MemoryStore},
1280 sync::UnreadNotificationsCount,
1281 };
1282
1283 #[test]
1284 fn test_room_info_serialization() {
1285 let info = RoomInfo {
1289 data_format_version: 1,
1290 room_id: room_id!("!gda78o:server.tld").into(),
1291 room_state: RoomState::Invited,
1292 notification_counts: UnreadNotificationsCount {
1293 highlight_count: 1,
1294 notification_count: 2,
1295 },
1296 summary: RoomSummary {
1297 room_heroes: vec![RoomHero {
1298 user_id: owned_user_id!("@somebody:example.org"),
1299 display_name: None,
1300 avatar_url: None,
1301 }],
1302 joined_member_count: 5,
1303 invited_member_count: 0,
1304 },
1305 members_synced: true,
1306 last_prev_batch: Some("pb".to_owned()),
1307 sync_info: SyncInfo::FullySynced,
1308 encryption_state_synced: true,
1309 latest_event_value: LatestEventValue::None,
1310 base_info: Box::new(
1311 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1312 ),
1313 read_receipts: Default::default(),
1314 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1315 cached_display_name: None,
1316 cached_user_defined_notification_mode: None,
1317 recency_stamp: Some(42.into()),
1318 invite_acceptance_details: None,
1319 };
1320
1321 let info_json = json!({
1322 "data_format_version": 1,
1323 "room_id": "!gda78o:server.tld",
1324 "room_state": "Invited",
1325 "notification_counts": {
1326 "highlight_count": 1,
1327 "notification_count": 2,
1328 },
1329 "summary": {
1330 "room_heroes": [{
1331 "user_id": "@somebody:example.org",
1332 "display_name": null,
1333 "avatar_url": null
1334 }],
1335 "joined_member_count": 5,
1336 "invited_member_count": 0,
1337 },
1338 "members_synced": true,
1339 "last_prev_batch": "pb",
1340 "sync_info": "FullySynced",
1341 "encryption_state_synced": true,
1342 "latest_event_value": "None",
1343 "base_info": {
1344 "avatar": null,
1345 "canonical_alias": null,
1346 "create": null,
1347 "dm_targets": [],
1348 "encryption": null,
1349 "guest_access": null,
1350 "history_visibility": null,
1351 "is_marked_unread": false,
1352 "is_marked_unread_source": "Unstable",
1353 "join_rules": null,
1354 "max_power_level": 100,
1355 "name": null,
1356 "tombstone": null,
1357 "topic": null,
1358 "pinned_events": {
1359 "pinned": ["$a"]
1360 },
1361 },
1362 "read_receipts": {
1363 "num_unread": 0,
1364 "num_mentions": 0,
1365 "num_notifications": 0,
1366 "latest_active": null,
1367 "pending": [],
1368 },
1369 "recency_stamp": 42,
1370 });
1371
1372 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1373 }
1374
1375 #[async_test]
1376 async fn test_room_info_migration_v1() {
1377 let store = MemoryStore::new().into_state_store();
1378
1379 let room_info_json = json!({
1380 "room_id": "!gda78o:server.tld",
1381 "room_state": "Joined",
1382 "notification_counts": {
1383 "highlight_count": 1,
1384 "notification_count": 2,
1385 },
1386 "summary": {
1387 "room_heroes": [{
1388 "user_id": "@somebody:example.org",
1389 "display_name": null,
1390 "avatar_url": null
1391 }],
1392 "joined_member_count": 5,
1393 "invited_member_count": 0,
1394 },
1395 "members_synced": true,
1396 "last_prev_batch": "pb",
1397 "sync_info": "FullySynced",
1398 "encryption_state_synced": true,
1399 "latest_event": {
1400 "event": {
1401 "encryption_info": null,
1402 "event": {
1403 "sender": "@u:i.uk",
1404 },
1405 },
1406 },
1407 "base_info": {
1408 "avatar": null,
1409 "canonical_alias": null,
1410 "create": null,
1411 "dm_targets": [],
1412 "encryption": null,
1413 "guest_access": null,
1414 "history_visibility": null,
1415 "join_rules": null,
1416 "max_power_level": 100,
1417 "name": null,
1418 "tombstone": null,
1419 "topic": null,
1420 },
1421 "read_receipts": {
1422 "num_unread": 0,
1423 "num_mentions": 0,
1424 "num_notifications": 0,
1425 "latest_active": null,
1426 "pending": []
1427 },
1428 "recency_stamp": 42,
1429 });
1430 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1431
1432 assert_eq!(room_info.data_format_version, 0);
1433 assert!(room_info.base_info.notable_tags.is_empty());
1434 assert!(room_info.base_info.pinned_events.is_none());
1435
1436 assert!(room_info.apply_migrations(store.clone()).await);
1438
1439 assert_eq!(room_info.data_format_version, 1);
1440 assert!(room_info.base_info.notable_tags.is_empty());
1441 assert!(room_info.base_info.pinned_events.is_none());
1442
1443 assert!(!room_info.apply_migrations(store.clone()).await);
1445
1446 assert_eq!(room_info.data_format_version, 1);
1447 assert!(room_info.base_info.notable_tags.is_empty());
1448 assert!(room_info.base_info.pinned_events.is_none());
1449
1450 let mut changes = StateChanges::default();
1452
1453 let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
1454 let tag_event = raw_tag_event.deserialize().unwrap();
1455 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1456
1457 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
1458 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1459 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1460
1461 store.save_changes(&changes).await.unwrap();
1462
1463 room_info.data_format_version = 0;
1465 assert!(room_info.apply_migrations(store.clone()).await);
1466
1467 assert_eq!(room_info.data_format_version, 1);
1468 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1469 assert!(room_info.base_info.pinned_events.is_some());
1470
1471 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1473 assert_eq!(new_room_info.data_format_version, 1);
1474 }
1475
1476 #[test]
1477 fn test_room_info_deserialization() {
1478 let info_json = json!({
1479 "room_id": "!gda78o:server.tld",
1480 "room_state": "Joined",
1481 "notification_counts": {
1482 "highlight_count": 1,
1483 "notification_count": 2,
1484 },
1485 "summary": {
1486 "room_heroes": [{
1487 "user_id": "@somebody:example.org",
1488 "display_name": "Somebody",
1489 "avatar_url": "mxc://example.org/abc"
1490 }],
1491 "joined_member_count": 5,
1492 "invited_member_count": 0,
1493 },
1494 "members_synced": true,
1495 "last_prev_batch": "pb",
1496 "sync_info": "FullySynced",
1497 "encryption_state_synced": true,
1498 "base_info": {
1499 "avatar": null,
1500 "canonical_alias": null,
1501 "create": null,
1502 "dm_targets": [],
1503 "encryption": null,
1504 "guest_access": null,
1505 "history_visibility": null,
1506 "join_rules": null,
1507 "max_power_level": 100,
1508 "name": null,
1509 "tombstone": null,
1510 "topic": null,
1511 },
1512 "cached_display_name": { "Calculated": "lol" },
1513 "cached_user_defined_notification_mode": "Mute",
1514 "recency_stamp": 42,
1515 });
1516
1517 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1518
1519 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1520 assert_eq!(info.room_state, RoomState::Joined);
1521 assert_eq!(info.notification_counts.highlight_count, 1);
1522 assert_eq!(info.notification_counts.notification_count, 2);
1523 assert_eq!(
1524 info.summary.room_heroes,
1525 vec![RoomHero {
1526 user_id: owned_user_id!("@somebody:example.org"),
1527 display_name: Some("Somebody".to_owned()),
1528 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1529 }]
1530 );
1531 assert_eq!(info.summary.joined_member_count, 5);
1532 assert_eq!(info.summary.invited_member_count, 0);
1533 assert!(info.members_synced);
1534 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1535 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1536 assert!(info.encryption_state_synced);
1537 assert_matches!(info.latest_event_value, LatestEventValue::None);
1538 assert!(info.base_info.avatar.is_none());
1539 assert!(info.base_info.canonical_alias.is_none());
1540 assert!(info.base_info.create.is_none());
1541 assert_eq!(info.base_info.dm_targets.len(), 0);
1542 assert!(info.base_info.encryption.is_none());
1543 assert!(info.base_info.guest_access.is_none());
1544 assert!(info.base_info.history_visibility.is_none());
1545 assert!(info.base_info.join_rules.is_none());
1546 assert_eq!(info.base_info.max_power_level, 100);
1547 assert!(info.base_info.name.is_none());
1548 assert!(info.base_info.tombstone.is_none());
1549 assert!(info.base_info.topic.is_none());
1550
1551 assert_eq!(
1552 info.cached_display_name.as_ref(),
1553 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1554 );
1555 assert_eq!(
1556 info.cached_user_defined_notification_mode.as_ref(),
1557 Some(&RoomNotificationMode::Mute)
1558 );
1559 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1560 }
1561
1562 #[test]
1569 fn test_room_info_deserialization_without_optional_items() {
1570 let info_json = json!({
1573 "room_id": "!gda78o:server.tld",
1574 "room_state": "Invited",
1575 "notification_counts": {
1576 "highlight_count": 1,
1577 "notification_count": 2,
1578 },
1579 "summary": {
1580 "room_heroes": [{
1581 "user_id": "@somebody:example.org",
1582 "display_name": "Somebody",
1583 "avatar_url": "mxc://example.org/abc"
1584 }],
1585 "joined_member_count": 5,
1586 "invited_member_count": 0,
1587 },
1588 "members_synced": true,
1589 "last_prev_batch": "pb",
1590 "sync_info": "FullySynced",
1591 "encryption_state_synced": true,
1592 "base_info": {
1593 "avatar": null,
1594 "canonical_alias": null,
1595 "create": null,
1596 "dm_targets": [],
1597 "encryption": null,
1598 "guest_access": null,
1599 "history_visibility": null,
1600 "join_rules": null,
1601 "max_power_level": 100,
1602 "name": null,
1603 "tombstone": null,
1604 "topic": null,
1605 },
1606 });
1607
1608 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1609
1610 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1611 assert_eq!(info.room_state, RoomState::Invited);
1612 assert_eq!(info.notification_counts.highlight_count, 1);
1613 assert_eq!(info.notification_counts.notification_count, 2);
1614 assert_eq!(
1615 info.summary.room_heroes,
1616 vec![RoomHero {
1617 user_id: owned_user_id!("@somebody:example.org"),
1618 display_name: Some("Somebody".to_owned()),
1619 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1620 }]
1621 );
1622 assert_eq!(info.summary.joined_member_count, 5);
1623 assert_eq!(info.summary.invited_member_count, 0);
1624 assert!(info.members_synced);
1625 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1626 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1627 assert!(info.encryption_state_synced);
1628 assert!(info.base_info.avatar.is_none());
1629 assert!(info.base_info.canonical_alias.is_none());
1630 assert!(info.base_info.create.is_none());
1631 assert_eq!(info.base_info.dm_targets.len(), 0);
1632 assert!(info.base_info.encryption.is_none());
1633 assert!(info.base_info.guest_access.is_none());
1634 assert!(info.base_info.history_visibility.is_none());
1635 assert!(info.base_info.join_rules.is_none());
1636 assert_eq!(info.base_info.max_power_level, 100);
1637 assert!(info.base_info.name.is_none());
1638 assert!(info.base_info.tombstone.is_none());
1639 assert!(info.base_info.topic.is_none());
1640 }
1641}