1use std::{
16 collections::{BTreeMap, HashSet},
17 sync::{Arc, atomic::AtomicBool},
18};
19
20use bitflags::bitflags;
21use eyeball::Subscriber;
22use matrix_sdk_common::{
23 ROOM_VERSION_FALLBACK, ROOM_VERSION_RULES_FALLBACK, deserialized_responses::TimelineEventKind,
24};
25use ruma::{
26 EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
27 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId,
28 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
29 assign,
30 events::{
31 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, StateEventType,
32 SyncStateEvent,
33 beacon_info::BeaconInfoEventContent,
34 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
35 direct::OwnedDirectUserIdentifier,
36 room::{
37 avatar::{self, RoomAvatarEventContent},
38 canonical_alias::RoomCanonicalAliasEventContent,
39 encryption::RoomEncryptionEventContent,
40 guest_access::{GuestAccess, RoomGuestAccessEventContent},
41 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
42 join_rules::{JoinRule, RoomJoinRulesEventContent},
43 name::RoomNameEventContent,
44 pinned_events::RoomPinnedEventsEventContent,
45 redaction::SyncRoomRedactionEvent,
46 tombstone::RoomTombstoneEventContent,
47 topic::RoomTopicEventContent,
48 },
49 tag::{TagEventContent, TagName, Tags},
50 },
51 room::RoomType,
52 room_version_rules::{AuthorizationRules, RedactionRules, RoomVersionRules},
53 serde::Raw,
54};
55use serde::{Deserialize, Serialize};
56use tracing::{debug, error, field::debug, info, instrument, warn};
57
58use super::{
59 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
60 RoomHero, RoomNotableTags, RoomState, RoomSummary,
61};
62use crate::{
63 MinimalStateEvent, OriginalMinimalStateEvent,
64 deserialized_responses::RawSyncOrStrippedState,
65 latest_event::{LatestEvent, LatestEventValue},
66 notification_settings::RoomNotificationMode,
67 read_receipts::RoomReadReceipts,
68 store::{DynStateStore, StateStoreExt},
69 sync::UnreadNotificationsCount,
70};
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct InviteAcceptanceDetails {
76 pub invite_accepted_at: MilliSecondsSinceUnixEpoch,
79
80 pub inviter: OwnedUserId,
82}
83
84impl Room {
85 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
87 self.info.subscribe()
88 }
89
90 pub fn clone_info(&self) -> RoomInfo {
92 self.info.get()
93 }
94
95 pub fn set_room_info(
97 &self,
98 room_info: RoomInfo,
99 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
100 ) {
101 self.info.set(room_info);
102
103 if !room_info_notable_update_reasons.is_empty() {
104 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
106 room_id: self.room_id.clone(),
107 reasons: room_info_notable_update_reasons,
108 });
109 } else {
110 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
114 room_id: self.room_id.clone(),
115 reasons: RoomInfoNotableUpdateReasons::NONE,
116 });
117 }
118 }
119}
120
121#[derive(Clone, Debug, Serialize, Deserialize)]
125pub struct BaseRoomInfo {
126 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
128 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
130 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
131 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
133 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
135 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
138 pub(crate) encryption: Option<RoomEncryptionEventContent>,
140 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
142 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
144 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
146 pub(crate) max_power_level: i64,
148 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
150 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
152 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
154 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
157 pub(crate) rtc_member_events:
158 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
159 #[serde(default)]
161 pub(crate) is_marked_unread: bool,
162 #[serde(default)]
164 pub(crate) is_marked_unread_source: AccountDataSource,
165 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
170 pub(crate) notable_tags: RoomNotableTags,
171 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
173}
174
175impl BaseRoomInfo {
176 pub fn new() -> Self {
178 Self::default()
179 }
180
181 pub fn room_version(&self) -> Option<&RoomVersionId> {
186 match self.create.as_ref()? {
187 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
188 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
189 }
190 }
191
192 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
196 match ev {
197 AnySyncStateEvent::BeaconInfo(b) => {
198 self.beacons.insert(b.state_key().clone(), b.into());
199 }
200 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
202 self.encryption = Some(encryption.content.clone());
203 }
204 AnySyncStateEvent::RoomAvatar(a) => {
205 self.avatar = Some(a.into());
206 }
207 AnySyncStateEvent::RoomName(n) => {
208 self.name = Some(n.into());
209 }
210 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
212 self.create = Some(c.into());
213 }
214 AnySyncStateEvent::RoomHistoryVisibility(h) => {
215 self.history_visibility = Some(h.into());
216 }
217 AnySyncStateEvent::RoomGuestAccess(g) => {
218 self.guest_access = Some(g.into());
219 }
220 AnySyncStateEvent::RoomJoinRules(c) => {
221 self.join_rules = Some(c.into());
222 }
223 AnySyncStateEvent::RoomCanonicalAlias(a) => {
224 self.canonical_alias = Some(a.into());
225 }
226 AnySyncStateEvent::RoomTopic(t) => {
227 self.topic = Some(t.into());
228 }
229 AnySyncStateEvent::RoomTombstone(t) => {
230 self.tombstone = Some(t.into());
231 }
232 AnySyncStateEvent::RoomPowerLevels(p) => {
233 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
235 }
236 AnySyncStateEvent::CallMember(m) => {
237 let Some(o_ev) = m.as_original() else {
238 return false;
239 };
240
241 let mut o_ev = o_ev.clone();
244 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
245
246 self.rtc_member_events
248 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
249
250 self.rtc_member_events.retain(|_, ev| {
252 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
253 });
254 }
255 AnySyncStateEvent::RoomPinnedEvents(p) => {
256 self.pinned_events = p.as_original().map(|p| p.content.clone());
257 }
258 _ => return false,
259 }
260
261 true
262 }
263
264 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
269 match ev {
270 AnyStrippedStateEvent::RoomEncryption(encryption) => {
271 if let Some(algorithm) = &encryption.content.algorithm {
272 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
273 rotation_period_ms: encryption.content.rotation_period_ms,
274 rotation_period_msgs: encryption.content.rotation_period_msgs,
275 });
276 self.encryption = Some(content);
277 }
278 }
282 AnyStrippedStateEvent::RoomAvatar(a) => {
283 self.avatar = Some(a.into());
284 }
285 AnyStrippedStateEvent::RoomName(n) => {
286 self.name = Some(n.into());
287 }
288 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
289 self.create = Some(c.into());
290 }
291 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
292 self.history_visibility = Some(h.into());
293 }
294 AnyStrippedStateEvent::RoomGuestAccess(g) => {
295 self.guest_access = Some(g.into());
296 }
297 AnyStrippedStateEvent::RoomJoinRules(c) => {
298 self.join_rules = Some(c.into());
299 }
300 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
301 self.canonical_alias = Some(a.into());
302 }
303 AnyStrippedStateEvent::RoomTopic(t) => {
304 self.topic = Some(t.into());
305 }
306 AnyStrippedStateEvent::RoomTombstone(t) => {
307 self.tombstone = Some(t.into());
308 }
309 AnyStrippedStateEvent::RoomPowerLevels(p) => {
310 self.max_power_level = p.power_levels(&AuthorizationRules::V1, vec![]).max().into();
312 }
313 AnyStrippedStateEvent::CallMember(_) => {
314 return false;
317 }
318 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
319 if let Some(pinned) = p.content.pinned.clone() {
320 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
321 }
322 }
323 _ => return false,
324 }
325
326 true
327 }
328
329 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
330 let redaction_rules = self
331 .room_version()
332 .and_then(|room_version| room_version.rules())
333 .unwrap_or(ROOM_VERSION_RULES_FALLBACK)
334 .redaction;
335
336 if let Some(ev) = &mut self.avatar
337 && ev.event_id() == Some(redacts)
338 {
339 ev.redact(&redaction_rules);
340 } else if let Some(ev) = &mut self.canonical_alias
341 && ev.event_id() == Some(redacts)
342 {
343 ev.redact(&redaction_rules);
344 } else if let Some(ev) = &mut self.create
345 && ev.event_id() == Some(redacts)
346 {
347 ev.redact(&redaction_rules);
348 } else if let Some(ev) = &mut self.guest_access
349 && ev.event_id() == Some(redacts)
350 {
351 ev.redact(&redaction_rules);
352 } else if let Some(ev) = &mut self.history_visibility
353 && ev.event_id() == Some(redacts)
354 {
355 ev.redact(&redaction_rules);
356 } else if let Some(ev) = &mut self.join_rules
357 && ev.event_id() == Some(redacts)
358 {
359 ev.redact(&redaction_rules);
360 } else if let Some(ev) = &mut self.name
361 && ev.event_id() == Some(redacts)
362 {
363 ev.redact(&redaction_rules);
364 } else if let Some(ev) = &mut self.tombstone
365 && ev.event_id() == Some(redacts)
366 {
367 ev.redact(&redaction_rules);
368 } else if let Some(ev) = &mut self.topic
369 && ev.event_id() == Some(redacts)
370 {
371 ev.redact(&redaction_rules);
372 } else {
373 self.rtc_member_events
374 .retain(|_, member_event| member_event.event_id() != Some(redacts));
375 }
376 }
377
378 pub fn handle_notable_tags(&mut self, tags: &Tags) {
379 let mut notable_tags = RoomNotableTags::empty();
380
381 if tags.contains_key(&TagName::Favorite) {
382 notable_tags.insert(RoomNotableTags::FAVOURITE);
383 }
384
385 if tags.contains_key(&TagName::LowPriority) {
386 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
387 }
388
389 self.notable_tags = notable_tags;
390 }
391}
392
393impl Default for BaseRoomInfo {
394 fn default() -> Self {
395 Self {
396 avatar: None,
397 beacons: BTreeMap::new(),
398 canonical_alias: None,
399 create: None,
400 dm_targets: Default::default(),
401 encryption: None,
402 guest_access: None,
403 history_visibility: None,
404 join_rules: None,
405 max_power_level: 100,
406 name: None,
407 tombstone: None,
408 topic: None,
409 rtc_member_events: BTreeMap::new(),
410 is_marked_unread: false,
411 is_marked_unread_source: AccountDataSource::Unstable,
412 notable_tags: RoomNotableTags::empty(),
413 pinned_events: None,
414 }
415 }
416}
417
418#[derive(Clone, Debug, Serialize, Deserialize)]
422pub struct RoomInfo {
423 #[serde(default, alias = "version")]
426 pub(crate) data_format_version: u8,
427
428 pub(crate) room_id: OwnedRoomId,
430
431 pub(crate) room_state: RoomState,
433
434 pub(crate) notification_counts: UnreadNotificationsCount,
439
440 pub(crate) summary: RoomSummary,
442
443 pub(crate) members_synced: bool,
445
446 pub(crate) last_prev_batch: Option<String>,
448
449 pub(crate) sync_info: SyncInfo,
451
452 pub(crate) encryption_state_synced: bool,
454
455 pub(crate) latest_event: Option<Box<LatestEvent>>,
459
460 #[serde(default)]
464 pub(crate) new_latest_event: LatestEventValue,
465
466 #[serde(default)]
468 pub(crate) read_receipts: RoomReadReceipts,
469
470 pub(crate) base_info: Box<BaseRoomInfo>,
473
474 #[serde(skip)]
478 pub(crate) warned_about_unknown_room_version_rules: Arc<AtomicBool>,
479
480 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub(crate) cached_display_name: Option<RoomDisplayName>,
486
487 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
490
491 #[serde(default)]
508 pub(crate) recency_stamp: Option<RoomRecencyStamp>,
509
510 #[serde(default, skip_serializing_if = "Option::is_none")]
516 pub(crate) invite_acceptance_details: Option<InviteAcceptanceDetails>,
517}
518
519impl RoomInfo {
520 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
522 Self {
523 data_format_version: 1,
524 room_id: room_id.into(),
525 room_state,
526 notification_counts: Default::default(),
527 summary: Default::default(),
528 members_synced: false,
529 last_prev_batch: None,
530 sync_info: SyncInfo::NoState,
531 encryption_state_synced: false,
532 latest_event: None,
533 new_latest_event: LatestEventValue::default(),
534 read_receipts: Default::default(),
535 base_info: Box::new(BaseRoomInfo::new()),
536 warned_about_unknown_room_version_rules: Arc::new(false.into()),
537 cached_display_name: None,
538 cached_user_defined_notification_mode: None,
539 recency_stamp: None,
540 invite_acceptance_details: None,
541 }
542 }
543
544 pub fn mark_as_joined(&mut self) {
546 self.set_state(RoomState::Joined);
547 }
548
549 pub fn mark_as_left(&mut self) {
551 self.set_state(RoomState::Left);
552 }
553
554 pub fn mark_as_invited(&mut self) {
556 self.set_state(RoomState::Invited);
557 }
558
559 pub fn mark_as_knocked(&mut self) {
561 self.set_state(RoomState::Knocked);
562 }
563
564 pub fn mark_as_banned(&mut self) {
566 self.set_state(RoomState::Banned);
567 }
568
569 pub fn set_state(&mut self, room_state: RoomState) {
571 if self.state() != RoomState::Joined && self.invite_acceptance_details.is_some() {
572 error!(room_id = %self.room_id, "The RoomInfo contains invite acceptance details but the room is not in the joined state");
573 }
574 self.invite_acceptance_details = None;
577 self.room_state = room_state;
578 }
579
580 pub fn mark_members_synced(&mut self) {
582 self.members_synced = true;
583 }
584
585 pub fn mark_members_missing(&mut self) {
587 self.members_synced = false;
588 }
589
590 pub fn are_members_synced(&self) -> bool {
592 self.members_synced
593 }
594
595 pub fn mark_state_partially_synced(&mut self) {
597 self.sync_info = SyncInfo::PartiallySynced;
598 }
599
600 pub fn mark_state_fully_synced(&mut self) {
602 self.sync_info = SyncInfo::FullySynced;
603 }
604
605 pub fn mark_state_not_synced(&mut self) {
607 self.sync_info = SyncInfo::NoState;
608 }
609
610 pub fn mark_encryption_state_synced(&mut self) {
612 self.encryption_state_synced = true;
613 }
614
615 pub fn mark_encryption_state_missing(&mut self) {
617 self.encryption_state_synced = false;
618 }
619
620 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
624 if self.last_prev_batch.as_deref() != prev_batch {
625 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
626 true
627 } else {
628 false
629 }
630 }
631
632 pub fn state(&self) -> RoomState {
634 self.room_state
635 }
636
637 #[cfg(not(feature = "experimental-encrypted-state-events"))]
639 pub fn encryption_state(&self) -> EncryptionState {
640 if !self.encryption_state_synced {
641 EncryptionState::Unknown
642 } else if self.base_info.encryption.is_some() {
643 EncryptionState::Encrypted
644 } else {
645 EncryptionState::NotEncrypted
646 }
647 }
648
649 #[cfg(feature = "experimental-encrypted-state-events")]
651 pub fn encryption_state(&self) -> EncryptionState {
652 if !self.encryption_state_synced {
653 EncryptionState::Unknown
654 } else {
655 self.base_info
656 .encryption
657 .as_ref()
658 .map(|state| {
659 if state.encrypt_state_events {
660 EncryptionState::StateEncrypted
661 } else {
662 EncryptionState::Encrypted
663 }
664 })
665 .unwrap_or(EncryptionState::NotEncrypted)
666 }
667 }
668
669 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
671 self.base_info.encryption = event;
672 }
673
674 pub fn handle_encryption_state(
676 &mut self,
677 requested_required_states: &[(StateEventType, String)],
678 ) {
679 if requested_required_states
680 .iter()
681 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
682 {
683 self.mark_encryption_state_synced();
689 }
690 }
691
692 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
696 let base_info_has_been_modified = self.base_info.handle_state_event(event);
698
699 if let AnySyncStateEvent::RoomEncryption(_) = event {
700 self.mark_encryption_state_synced();
706 }
707
708 base_info_has_been_modified
709 }
710
711 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
715 self.base_info.handle_stripped_state_event(event)
716 }
717
718 #[instrument(skip_all, fields(redacts))]
720 pub fn handle_redaction(
721 &mut self,
722 event: &SyncRoomRedactionEvent,
723 _raw: &Raw<SyncRoomRedactionEvent>,
724 ) {
725 let redaction_rules = self.room_version_rules_or_default().redaction;
726
727 let Some(redacts) = event.redacts(&redaction_rules) else {
728 info!("Can't apply redaction, redacts field is missing");
729 return;
730 };
731 tracing::Span::current().record("redacts", debug(redacts));
732
733 if let Some(latest_event) = &mut self.latest_event {
734 tracing::trace!("Checking if redaction applies to latest event");
735 if latest_event.event_id().as_deref() == Some(redacts) {
736 match apply_redaction(latest_event.event().raw(), _raw, &redaction_rules) {
737 Some(redacted) => {
738 latest_event.event_mut().kind =
741 TimelineEventKind::PlainText { event: redacted };
742 debug!("Redacted latest event");
743 }
744 None => {
745 self.latest_event = None;
746 debug!("Removed latest event");
747 }
748 }
749 }
750 }
751
752 self.base_info.handle_redaction(redacts);
753 }
754
755 pub fn avatar_url(&self) -> Option<&MxcUri> {
757 self.base_info
758 .avatar
759 .as_ref()
760 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
761 }
762
763 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
765 self.base_info.avatar = url.map(|url| {
766 let mut content = RoomAvatarEventContent::new();
767 content.url = Some(url);
768
769 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
770 });
771 }
772
773 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
775 self.base_info
776 .avatar
777 .as_ref()
778 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
779 }
780
781 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
783 self.notification_counts = notification_counts;
784 }
785
786 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
790 let mut changed = false;
791
792 if !summary.is_empty() {
793 if !summary.heroes.is_empty() {
794 self.summary.room_heroes = summary
795 .heroes
796 .iter()
797 .map(|hero_id| RoomHero {
798 user_id: hero_id.to_owned(),
799 display_name: None,
800 avatar_url: None,
801 })
802 .collect();
803
804 changed = true;
805 }
806
807 if let Some(joined) = summary.joined_member_count {
808 self.summary.joined_member_count = joined.into();
809 changed = true;
810 }
811
812 if let Some(invited) = summary.invited_member_count {
813 self.summary.invited_member_count = invited.into();
814 changed = true;
815 }
816 }
817
818 changed
819 }
820
821 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
823 self.summary.joined_member_count = count;
824 }
825
826 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
828 self.summary.invited_member_count = count;
829 }
830
831 pub(crate) fn set_invite_acceptance_details(&mut self, details: InviteAcceptanceDetails) {
832 self.invite_acceptance_details = Some(details);
833 }
834
835 pub fn invite_acceptance_details(&self) -> Option<InviteAcceptanceDetails> {
842 self.invite_acceptance_details.clone()
843 }
844
845 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
847 self.summary.room_heroes = heroes;
848 }
849
850 pub fn heroes(&self) -> &[RoomHero] {
852 &self.summary.room_heroes
853 }
854
855 pub fn active_members_count(&self) -> u64 {
859 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
860 }
861
862 pub fn invited_members_count(&self) -> u64 {
864 self.summary.invited_member_count
865 }
866
867 pub fn joined_members_count(&self) -> u64 {
869 self.summary.joined_member_count
870 }
871
872 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
874 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
875 }
876
877 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
879 self.base_info
880 .canonical_alias
881 .as_ref()
882 .and_then(|ev| ev.as_original())
883 .map(|ev| ev.content.alt_aliases.as_ref())
884 .unwrap_or_default()
885 }
886
887 pub fn room_id(&self) -> &RoomId {
889 &self.room_id
890 }
891
892 pub fn room_version(&self) -> Option<&RoomVersionId> {
894 self.base_info.room_version()
895 }
896
897 pub fn room_version_rules_or_default(&self) -> RoomVersionRules {
902 use std::sync::atomic::Ordering;
903
904 self.base_info.room_version().and_then(|room_version| room_version.rules()).unwrap_or_else(
905 || {
906 if self
907 .warned_about_unknown_room_version_rules
908 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
909 .is_ok()
910 {
911 warn!("Unable to get the room version rules, defaulting to rules for room version {ROOM_VERSION_FALLBACK}");
912 }
913
914 ROOM_VERSION_RULES_FALLBACK
915 },
916 )
917 }
918
919 pub fn room_type(&self) -> Option<&RoomType> {
921 match self.base_info.create.as_ref()? {
922 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
923 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
924 }
925 }
926
927 pub fn creators(&self) -> Option<Vec<OwnedUserId>> {
929 match self.base_info.create.as_ref()? {
930 MinimalStateEvent::Original(ev) => Some(ev.content.creators()),
931 MinimalStateEvent::Redacted(ev) => Some(ev.content.creators()),
932 }
933 }
934
935 pub(super) fn guest_access(&self) -> &GuestAccess {
936 match &self.base_info.guest_access {
937 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
938 _ => &GuestAccess::Forbidden,
939 }
940 }
941
942 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
946 match &self.base_info.history_visibility {
947 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
948 _ => None,
949 }
950 }
951
952 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
959 match &self.base_info.history_visibility {
960 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
961 _ => &HistoryVisibility::Shared,
962 }
963 }
964
965 pub fn join_rule(&self) -> Option<&JoinRule> {
968 match &self.base_info.join_rules {
969 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
970 _ => None,
971 }
972 }
973
974 pub fn name(&self) -> Option<&str> {
976 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
977 (!name.is_empty()).then_some(name)
978 }
979
980 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
982 Some(&self.base_info.create.as_ref()?.as_original()?.content)
983 }
984
985 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
987 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
988 }
989
990 pub fn topic(&self) -> Option<&str> {
992 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
993 }
994
995 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1000 let mut v = self
1001 .base_info
1002 .rtc_member_events
1003 .iter()
1004 .filter_map(|(user_id, ev)| {
1005 ev.as_original().map(|ev| {
1006 ev.content
1007 .active_memberships(None)
1008 .into_iter()
1009 .map(move |m| (user_id.clone(), m))
1010 })
1011 })
1012 .flatten()
1013 .collect::<Vec<_>>();
1014 v.sort_by_key(|(_, m)| m.created_ts());
1015 v
1016 }
1017
1018 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
1024 self.active_matrix_rtc_memberships()
1025 .into_iter()
1026 .filter(|(_user_id, m)| m.is_room_call())
1027 .collect()
1028 }
1029
1030 pub fn has_active_room_call(&self) -> bool {
1033 !self.active_room_call_memberships().is_empty()
1034 }
1035
1036 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
1045 self.active_room_call_memberships()
1046 .iter()
1047 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
1048 .collect()
1049 }
1050
1051 pub fn latest_event(&self) -> Option<&LatestEvent> {
1053 self.latest_event.as_deref()
1054 }
1055
1056 pub fn set_new_latest_event(&mut self, new_value: LatestEventValue) {
1058 self.new_latest_event = new_value;
1059 }
1060
1061 pub fn update_recency_stamp(&mut self, stamp: RoomRecencyStamp) {
1065 self.recency_stamp = Some(stamp);
1066 }
1067
1068 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
1070 self.base_info.pinned_events.clone().map(|c| c.pinned)
1071 }
1072
1073 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1079 self.base_info
1080 .pinned_events
1081 .as_ref()
1082 .map(|p| p.pinned.contains(&event_id.to_owned()))
1083 .unwrap_or_default()
1084 }
1085
1086 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1094 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1095 let mut migrated = false;
1096
1097 if self.data_format_version < 1 {
1098 info!("Migrating room info to version 1");
1099
1100 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1102 Ok(Some(raw_event)) => match raw_event.deserialize() {
1104 Ok(event) => {
1105 self.base_info.handle_notable_tags(&event.content.tags);
1106 }
1107 Err(error) => {
1108 warn!("Failed to deserialize room tags: {error}");
1109 }
1110 },
1111 Ok(_) => {
1112 }
1114 Err(error) => {
1115 warn!("Failed to load room tags: {error}");
1116 }
1117 }
1118
1119 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1121 {
1122 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1124 match raw_event.deserialize() {
1125 Ok(event) => {
1126 self.handle_state_event(&event.into());
1127 }
1128 Err(error) => {
1129 warn!("Failed to deserialize room pinned events: {error}");
1130 }
1131 }
1132 }
1133 Ok(_) => {
1134 }
1136 Err(error) => {
1137 warn!("Failed to load room pinned events: {error}");
1138 }
1139 }
1140
1141 self.data_format_version = 1;
1142 migrated = true;
1143 }
1144
1145 migrated
1146 }
1147}
1148
1149#[repr(transparent)]
1151#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1152#[serde(transparent)]
1153pub struct RoomRecencyStamp(u64);
1154
1155impl From<u64> for RoomRecencyStamp {
1156 fn from(value: u64) -> Self {
1157 Self(value)
1158 }
1159}
1160
1161impl From<RoomRecencyStamp> for u64 {
1162 fn from(value: RoomRecencyStamp) -> Self {
1163 value.0
1164 }
1165}
1166
1167#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1168pub(crate) enum SyncInfo {
1169 NoState,
1175
1176 PartiallySynced,
1179
1180 FullySynced,
1182}
1183
1184pub fn apply_redaction(
1187 event: &Raw<AnySyncTimelineEvent>,
1188 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1189 rules: &RedactionRules,
1190) -> Option<Raw<AnySyncTimelineEvent>> {
1191 use ruma::canonical_json::{RedactedBecause, redact_in_place};
1192
1193 let mut event_json = match event.deserialize_as() {
1194 Ok(json) => json,
1195 Err(e) => {
1196 warn!("Failed to deserialize latest event: {e}");
1197 return None;
1198 }
1199 };
1200
1201 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1202 Ok(rb) => rb,
1203 Err(e) => {
1204 warn!("Redaction event is not valid canonical JSON: {e}");
1205 return None;
1206 }
1207 };
1208
1209 let redact_result = redact_in_place(&mut event_json, rules, Some(redacted_because));
1210
1211 if let Err(e) = redact_result {
1212 warn!("Failed to redact event: {e}");
1213 return None;
1214 }
1215
1216 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1217 Some(raw.cast_unchecked())
1218}
1219
1220#[derive(Debug, Clone)]
1230pub struct RoomInfoNotableUpdate {
1231 pub room_id: OwnedRoomId,
1233
1234 pub reasons: RoomInfoNotableUpdateReasons,
1236}
1237
1238bitflags! {
1239 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1241 pub struct RoomInfoNotableUpdateReasons: u8 {
1242 const RECENCY_STAMP = 0b0000_0001;
1244
1245 const LATEST_EVENT = 0b0000_0010;
1247
1248 const READ_RECEIPT = 0b0000_0100;
1250
1251 const UNREAD_MARKER = 0b0000_1000;
1253
1254 const MEMBERSHIP = 0b0001_0000;
1256
1257 const DISPLAY_NAME = 0b0010_0000;
1259
1260 const NONE = 0b1000_0000;
1271 }
1272}
1273
1274impl Default for RoomInfoNotableUpdateReasons {
1275 fn default() -> Self {
1276 Self::empty()
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use std::sync::Arc;
1283
1284 use assert_matches::assert_matches;
1285 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1286 use matrix_sdk_test::{
1287 async_test,
1288 test_json::{TAG, sync_events::PINNED_EVENTS},
1289 };
1290 use ruma::{
1291 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1292 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1293 };
1294 use serde_json::json;
1295 use similar_asserts::assert_eq;
1296
1297 use super::{BaseRoomInfo, LatestEventValue, RoomInfo, SyncInfo};
1298 use crate::{
1299 RoomDisplayName, RoomHero, RoomState, StateChanges,
1300 latest_event::LatestEvent,
1301 notification_settings::RoomNotificationMode,
1302 room::{RoomNotableTags, RoomSummary},
1303 store::{IntoStateStore, MemoryStore},
1304 sync::UnreadNotificationsCount,
1305 };
1306
1307 #[test]
1308 fn test_room_info_serialization() {
1309 let info = RoomInfo {
1313 data_format_version: 1,
1314 room_id: room_id!("!gda78o:server.tld").into(),
1315 room_state: RoomState::Invited,
1316 notification_counts: UnreadNotificationsCount {
1317 highlight_count: 1,
1318 notification_count: 2,
1319 },
1320 summary: RoomSummary {
1321 room_heroes: vec![RoomHero {
1322 user_id: owned_user_id!("@somebody:example.org"),
1323 display_name: None,
1324 avatar_url: None,
1325 }],
1326 joined_member_count: 5,
1327 invited_member_count: 0,
1328 },
1329 members_synced: true,
1330 last_prev_batch: Some("pb".to_owned()),
1331 sync_info: SyncInfo::FullySynced,
1332 encryption_state_synced: true,
1333 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
1334 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1335 )))),
1336 new_latest_event: LatestEventValue::None,
1337 base_info: Box::new(
1338 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1339 ),
1340 read_receipts: Default::default(),
1341 warned_about_unknown_room_version_rules: Arc::new(false.into()),
1342 cached_display_name: None,
1343 cached_user_defined_notification_mode: None,
1344 recency_stamp: Some(42.into()),
1345 invite_acceptance_details: None,
1346 };
1347
1348 let info_json = json!({
1349 "data_format_version": 1,
1350 "room_id": "!gda78o:server.tld",
1351 "room_state": "Invited",
1352 "notification_counts": {
1353 "highlight_count": 1,
1354 "notification_count": 2,
1355 },
1356 "summary": {
1357 "room_heroes": [{
1358 "user_id": "@somebody:example.org",
1359 "display_name": null,
1360 "avatar_url": null
1361 }],
1362 "joined_member_count": 5,
1363 "invited_member_count": 0,
1364 },
1365 "members_synced": true,
1366 "last_prev_batch": "pb",
1367 "sync_info": "FullySynced",
1368 "encryption_state_synced": true,
1369 "latest_event": {
1370 "event": {
1371 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1372 "thread_summary": "None",
1373 "timestamp": null,
1374 },
1375 },
1376 "new_latest_event": "None",
1377 "base_info": {
1378 "avatar": null,
1379 "canonical_alias": null,
1380 "create": null,
1381 "dm_targets": [],
1382 "encryption": null,
1383 "guest_access": null,
1384 "history_visibility": null,
1385 "is_marked_unread": false,
1386 "is_marked_unread_source": "Unstable",
1387 "join_rules": null,
1388 "max_power_level": 100,
1389 "name": null,
1390 "tombstone": null,
1391 "topic": null,
1392 "pinned_events": {
1393 "pinned": ["$a"]
1394 },
1395 },
1396 "read_receipts": {
1397 "num_unread": 0,
1398 "num_mentions": 0,
1399 "num_notifications": 0,
1400 "latest_active": null,
1401 "pending": [],
1402 },
1403 "recency_stamp": 42,
1404 });
1405
1406 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1407 }
1408
1409 #[async_test]
1410 async fn test_room_info_migration_v1() {
1411 let store = MemoryStore::new().into_state_store();
1412
1413 let room_info_json = json!({
1414 "room_id": "!gda78o:server.tld",
1415 "room_state": "Joined",
1416 "notification_counts": {
1417 "highlight_count": 1,
1418 "notification_count": 2,
1419 },
1420 "summary": {
1421 "room_heroes": [{
1422 "user_id": "@somebody:example.org",
1423 "display_name": null,
1424 "avatar_url": null
1425 }],
1426 "joined_member_count": 5,
1427 "invited_member_count": 0,
1428 },
1429 "members_synced": true,
1430 "last_prev_batch": "pb",
1431 "sync_info": "FullySynced",
1432 "encryption_state_synced": true,
1433 "latest_event": {
1434 "event": {
1435 "encryption_info": null,
1436 "event": {
1437 "sender": "@u:i.uk",
1438 },
1439 },
1440 },
1441 "base_info": {
1442 "avatar": null,
1443 "canonical_alias": null,
1444 "create": null,
1445 "dm_targets": [],
1446 "encryption": null,
1447 "guest_access": null,
1448 "history_visibility": null,
1449 "join_rules": null,
1450 "max_power_level": 100,
1451 "name": null,
1452 "tombstone": null,
1453 "topic": null,
1454 },
1455 "read_receipts": {
1456 "num_unread": 0,
1457 "num_mentions": 0,
1458 "num_notifications": 0,
1459 "latest_active": null,
1460 "pending": []
1461 },
1462 "recency_stamp": 42,
1463 });
1464 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1465
1466 assert_eq!(room_info.data_format_version, 0);
1467 assert!(room_info.base_info.notable_tags.is_empty());
1468 assert!(room_info.base_info.pinned_events.is_none());
1469
1470 assert!(room_info.apply_migrations(store.clone()).await);
1472
1473 assert_eq!(room_info.data_format_version, 1);
1474 assert!(room_info.base_info.notable_tags.is_empty());
1475 assert!(room_info.base_info.pinned_events.is_none());
1476
1477 assert!(!room_info.apply_migrations(store.clone()).await);
1479
1480 assert_eq!(room_info.data_format_version, 1);
1481 assert!(room_info.base_info.notable_tags.is_empty());
1482 assert!(room_info.base_info.pinned_events.is_none());
1483
1484 let mut changes = StateChanges::default();
1486
1487 let raw_tag_event = Raw::new(&*TAG).unwrap().cast_unchecked();
1488 let tag_event = raw_tag_event.deserialize().unwrap();
1489 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1490
1491 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast_unchecked();
1492 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1493 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1494
1495 store.save_changes(&changes).await.unwrap();
1496
1497 room_info.data_format_version = 0;
1499 assert!(room_info.apply_migrations(store.clone()).await);
1500
1501 assert_eq!(room_info.data_format_version, 1);
1502 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1503 assert!(room_info.base_info.pinned_events.is_some());
1504
1505 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1507 assert_eq!(new_room_info.data_format_version, 1);
1508 }
1509
1510 #[test]
1511 fn test_room_info_deserialization() {
1512 let info_json = json!({
1513 "room_id": "!gda78o:server.tld",
1514 "room_state": "Joined",
1515 "notification_counts": {
1516 "highlight_count": 1,
1517 "notification_count": 2,
1518 },
1519 "summary": {
1520 "room_heroes": [{
1521 "user_id": "@somebody:example.org",
1522 "display_name": "Somebody",
1523 "avatar_url": "mxc://example.org/abc"
1524 }],
1525 "joined_member_count": 5,
1526 "invited_member_count": 0,
1527 },
1528 "members_synced": true,
1529 "last_prev_batch": "pb",
1530 "sync_info": "FullySynced",
1531 "encryption_state_synced": true,
1532 "base_info": {
1533 "avatar": null,
1534 "canonical_alias": null,
1535 "create": null,
1536 "dm_targets": [],
1537 "encryption": null,
1538 "guest_access": null,
1539 "history_visibility": null,
1540 "join_rules": null,
1541 "max_power_level": 100,
1542 "name": null,
1543 "tombstone": null,
1544 "topic": null,
1545 },
1546 "cached_display_name": { "Calculated": "lol" },
1547 "cached_user_defined_notification_mode": "Mute",
1548 "recency_stamp": 42,
1549 });
1550
1551 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1552
1553 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1554 assert_eq!(info.room_state, RoomState::Joined);
1555 assert_eq!(info.notification_counts.highlight_count, 1);
1556 assert_eq!(info.notification_counts.notification_count, 2);
1557 assert_eq!(
1558 info.summary.room_heroes,
1559 vec![RoomHero {
1560 user_id: owned_user_id!("@somebody:example.org"),
1561 display_name: Some("Somebody".to_owned()),
1562 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1563 }]
1564 );
1565 assert_eq!(info.summary.joined_member_count, 5);
1566 assert_eq!(info.summary.invited_member_count, 0);
1567 assert!(info.members_synced);
1568 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1569 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1570 assert!(info.encryption_state_synced);
1571 assert!(info.latest_event.is_none());
1572 assert_matches!(info.new_latest_event, LatestEventValue::None);
1573 assert!(info.base_info.avatar.is_none());
1574 assert!(info.base_info.canonical_alias.is_none());
1575 assert!(info.base_info.create.is_none());
1576 assert_eq!(info.base_info.dm_targets.len(), 0);
1577 assert!(info.base_info.encryption.is_none());
1578 assert!(info.base_info.guest_access.is_none());
1579 assert!(info.base_info.history_visibility.is_none());
1580 assert!(info.base_info.join_rules.is_none());
1581 assert_eq!(info.base_info.max_power_level, 100);
1582 assert!(info.base_info.name.is_none());
1583 assert!(info.base_info.tombstone.is_none());
1584 assert!(info.base_info.topic.is_none());
1585
1586 assert_eq!(
1587 info.cached_display_name.as_ref(),
1588 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1589 );
1590 assert_eq!(
1591 info.cached_user_defined_notification_mode.as_ref(),
1592 Some(&RoomNotificationMode::Mute)
1593 );
1594 assert_eq!(info.recency_stamp.as_ref(), Some(&42.into()));
1595 }
1596
1597 #[test]
1604 fn test_room_info_deserialization_without_optional_items() {
1605 let info_json = json!({
1608 "room_id": "!gda78o:server.tld",
1609 "room_state": "Invited",
1610 "notification_counts": {
1611 "highlight_count": 1,
1612 "notification_count": 2,
1613 },
1614 "summary": {
1615 "room_heroes": [{
1616 "user_id": "@somebody:example.org",
1617 "display_name": "Somebody",
1618 "avatar_url": "mxc://example.org/abc"
1619 }],
1620 "joined_member_count": 5,
1621 "invited_member_count": 0,
1622 },
1623 "members_synced": true,
1624 "last_prev_batch": "pb",
1625 "sync_info": "FullySynced",
1626 "encryption_state_synced": true,
1627 "base_info": {
1628 "avatar": null,
1629 "canonical_alias": null,
1630 "create": null,
1631 "dm_targets": [],
1632 "encryption": null,
1633 "guest_access": null,
1634 "history_visibility": null,
1635 "join_rules": null,
1636 "max_power_level": 100,
1637 "name": null,
1638 "tombstone": null,
1639 "topic": null,
1640 },
1641 });
1642
1643 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1644
1645 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1646 assert_eq!(info.room_state, RoomState::Invited);
1647 assert_eq!(info.notification_counts.highlight_count, 1);
1648 assert_eq!(info.notification_counts.notification_count, 2);
1649 assert_eq!(
1650 info.summary.room_heroes,
1651 vec![RoomHero {
1652 user_id: owned_user_id!("@somebody:example.org"),
1653 display_name: Some("Somebody".to_owned()),
1654 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1655 }]
1656 );
1657 assert_eq!(info.summary.joined_member_count, 5);
1658 assert_eq!(info.summary.invited_member_count, 0);
1659 assert!(info.members_synced);
1660 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1661 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1662 assert!(info.encryption_state_synced);
1663 assert!(info.base_info.avatar.is_none());
1664 assert!(info.base_info.canonical_alias.is_none());
1665 assert!(info.base_info.create.is_none());
1666 assert_eq!(info.base_info.dm_targets.len(), 0);
1667 assert!(info.base_info.encryption.is_none());
1668 assert!(info.base_info.guest_access.is_none());
1669 assert!(info.base_info.history_visibility.is_none());
1670 assert!(info.base_info.join_rules.is_none());
1671 assert_eq!(info.base_info.max_power_level, 100);
1672 assert!(info.base_info.name.is_none());
1673 assert!(info.base_info.tombstone.is_none());
1674 assert!(info.base_info.topic.is_none());
1675 }
1676}