1use std::{
16 collections::{BTreeMap, HashSet},
17 sync::{atomic::AtomicBool, Arc},
18};
19
20use bitflags::bitflags;
21use eyeball::Subscriber;
22use matrix_sdk_common::{deserialized_responses::TimelineEventKind, ROOM_VERSION_FALLBACK};
23use ruma::{
24 api::client::sync::sync_events::v3::RoomSummary as RumaSummary,
25 assign,
26 events::{
27 beacon_info::BeaconInfoEventContent,
28 call::member::{CallMemberEventContent, CallMemberStateKey, MembershipData},
29 direct::OwnedDirectUserIdentifier,
30 room::{
31 avatar::{self, RoomAvatarEventContent},
32 canonical_alias::RoomCanonicalAliasEventContent,
33 encryption::RoomEncryptionEventContent,
34 guest_access::{GuestAccess, RoomGuestAccessEventContent},
35 history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
36 join_rules::{JoinRule, RoomJoinRulesEventContent},
37 name::RoomNameEventContent,
38 pinned_events::RoomPinnedEventsEventContent,
39 redaction::SyncRoomRedactionEvent,
40 tombstone::RoomTombstoneEventContent,
41 topic::RoomTopicEventContent,
42 },
43 tag::{TagEventContent, TagName, Tags},
44 AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, RedactContent,
45 RedactedStateEventContent, StateEventType, StaticStateEventContent, SyncStateEvent,
46 },
47 room::RoomType,
48 serde::Raw,
49 EventId, MilliSecondsSinceUnixEpoch, MxcUri, OwnedEventId, OwnedMxcUri, OwnedRoomAliasId,
50 OwnedRoomId, OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
51};
52use serde::{Deserialize, Serialize};
53use tracing::{debug, field::debug, info, instrument, warn};
54
55use super::{
56 AccountDataSource, EncryptionState, Room, RoomCreateWithCreatorEventContent, RoomDisplayName,
57 RoomHero, RoomNotableTags, RoomState, RoomSummary,
58};
59use crate::{
60 deserialized_responses::RawSyncOrStrippedState,
61 latest_event::LatestEvent,
62 notification_settings::RoomNotificationMode,
63 read_receipts::RoomReadReceipts,
64 store::{DynStateStore, StateStoreExt},
65 sync::UnreadNotificationsCount,
66 MinimalStateEvent, OriginalMinimalStateEvent,
67};
68
69impl Room {
70 pub fn subscribe_info(&self) -> Subscriber<RoomInfo> {
72 self.inner.subscribe()
73 }
74
75 pub fn clone_info(&self) -> RoomInfo {
77 self.inner.get()
78 }
79
80 pub fn set_room_info(
82 &self,
83 room_info: RoomInfo,
84 room_info_notable_update_reasons: RoomInfoNotableUpdateReasons,
85 ) {
86 self.inner.set(room_info);
87
88 if !room_info_notable_update_reasons.is_empty() {
89 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
91 room_id: self.room_id.clone(),
92 reasons: room_info_notable_update_reasons,
93 });
94 } else {
95 let _ = self.room_info_notable_update_sender.send(RoomInfoNotableUpdate {
99 room_id: self.room_id.clone(),
100 reasons: RoomInfoNotableUpdateReasons::NONE,
101 });
102 }
103 }
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct BaseRoomInfo {
111 pub(crate) avatar: Option<MinimalStateEvent<RoomAvatarEventContent>>,
113 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
115 pub(crate) beacons: BTreeMap<OwnedUserId, MinimalStateEvent<BeaconInfoEventContent>>,
116 pub(crate) canonical_alias: Option<MinimalStateEvent<RoomCanonicalAliasEventContent>>,
118 pub(crate) create: Option<MinimalStateEvent<RoomCreateWithCreatorEventContent>>,
120 pub(crate) dm_targets: HashSet<OwnedDirectUserIdentifier>,
123 pub(crate) encryption: Option<RoomEncryptionEventContent>,
125 pub(crate) guest_access: Option<MinimalStateEvent<RoomGuestAccessEventContent>>,
127 pub(crate) history_visibility: Option<MinimalStateEvent<RoomHistoryVisibilityEventContent>>,
129 pub(crate) join_rules: Option<MinimalStateEvent<RoomJoinRulesEventContent>>,
131 pub(crate) max_power_level: i64,
133 pub(crate) name: Option<MinimalStateEvent<RoomNameEventContent>>,
135 pub(crate) tombstone: Option<MinimalStateEvent<RoomTombstoneEventContent>>,
137 pub(crate) topic: Option<MinimalStateEvent<RoomTopicEventContent>>,
139 #[serde(skip_serializing_if = "BTreeMap::is_empty", default)]
142 pub(crate) rtc_member_events:
143 BTreeMap<CallMemberStateKey, MinimalStateEvent<CallMemberEventContent>>,
144 #[serde(default)]
146 pub(crate) is_marked_unread: bool,
147 #[serde(default)]
149 pub(crate) is_marked_unread_source: AccountDataSource,
150 #[serde(skip_serializing_if = "RoomNotableTags::is_empty", default)]
155 pub(crate) notable_tags: RoomNotableTags,
156 pub(crate) pinned_events: Option<RoomPinnedEventsEventContent>,
158}
159
160impl BaseRoomInfo {
161 pub fn new() -> Self {
163 Self::default()
164 }
165
166 pub fn room_version(&self) -> Option<&RoomVersionId> {
171 match self.create.as_ref()? {
172 MinimalStateEvent::Original(ev) => Some(&ev.content.room_version),
173 MinimalStateEvent::Redacted(ev) => Some(&ev.content.room_version),
174 }
175 }
176
177 pub fn handle_state_event(&mut self, ev: &AnySyncStateEvent) -> bool {
181 match ev {
182 AnySyncStateEvent::BeaconInfo(b) => {
183 self.beacons.insert(b.state_key().clone(), b.into());
184 }
185 AnySyncStateEvent::RoomEncryption(SyncStateEvent::Original(encryption)) => {
187 self.encryption = Some(encryption.content.clone());
188 }
189 AnySyncStateEvent::RoomAvatar(a) => {
190 self.avatar = Some(a.into());
191 }
192 AnySyncStateEvent::RoomName(n) => {
193 self.name = Some(n.into());
194 }
195 AnySyncStateEvent::RoomCreate(c) if self.create.is_none() => {
197 self.create = Some(c.into());
198 }
199 AnySyncStateEvent::RoomHistoryVisibility(h) => {
200 self.history_visibility = Some(h.into());
201 }
202 AnySyncStateEvent::RoomGuestAccess(g) => {
203 self.guest_access = Some(g.into());
204 }
205 AnySyncStateEvent::RoomJoinRules(c) => {
206 self.join_rules = Some(c.into());
207 }
208 AnySyncStateEvent::RoomCanonicalAlias(a) => {
209 self.canonical_alias = Some(a.into());
210 }
211 AnySyncStateEvent::RoomTopic(t) => {
212 self.topic = Some(t.into());
213 }
214 AnySyncStateEvent::RoomTombstone(t) => {
215 self.tombstone = Some(t.into());
216 }
217 AnySyncStateEvent::RoomPowerLevels(p) => {
218 self.max_power_level = p.power_levels().max().into();
219 }
220 AnySyncStateEvent::CallMember(m) => {
221 let Some(o_ev) = m.as_original() else {
222 return false;
223 };
224
225 let mut o_ev = o_ev.clone();
228 o_ev.content.set_created_ts_if_none(o_ev.origin_server_ts);
229
230 self.rtc_member_events
232 .insert(m.state_key().clone(), SyncStateEvent::Original(o_ev).into());
233
234 self.rtc_member_events.retain(|_, ev| {
236 ev.as_original().is_some_and(|o| !o.content.active_memberships(None).is_empty())
237 });
238 }
239 AnySyncStateEvent::RoomPinnedEvents(p) => {
240 self.pinned_events = p.as_original().map(|p| p.content.clone());
241 }
242 _ => return false,
243 }
244
245 true
246 }
247
248 pub fn handle_stripped_state_event(&mut self, ev: &AnyStrippedStateEvent) -> bool {
253 match ev {
254 AnyStrippedStateEvent::RoomEncryption(encryption) => {
255 if let Some(algorithm) = &encryption.content.algorithm {
256 let content = assign!(RoomEncryptionEventContent::new(algorithm.clone()), {
257 rotation_period_ms: encryption.content.rotation_period_ms,
258 rotation_period_msgs: encryption.content.rotation_period_msgs,
259 });
260 self.encryption = Some(content);
261 }
262 }
266 AnyStrippedStateEvent::RoomAvatar(a) => {
267 self.avatar = Some(a.into());
268 }
269 AnyStrippedStateEvent::RoomName(n) => {
270 self.name = Some(n.into());
271 }
272 AnyStrippedStateEvent::RoomCreate(c) if self.create.is_none() => {
273 self.create = Some(c.into());
274 }
275 AnyStrippedStateEvent::RoomHistoryVisibility(h) => {
276 self.history_visibility = Some(h.into());
277 }
278 AnyStrippedStateEvent::RoomGuestAccess(g) => {
279 self.guest_access = Some(g.into());
280 }
281 AnyStrippedStateEvent::RoomJoinRules(c) => {
282 self.join_rules = Some(c.into());
283 }
284 AnyStrippedStateEvent::RoomCanonicalAlias(a) => {
285 self.canonical_alias = Some(a.into());
286 }
287 AnyStrippedStateEvent::RoomTopic(t) => {
288 self.topic = Some(t.into());
289 }
290 AnyStrippedStateEvent::RoomTombstone(t) => {
291 self.tombstone = Some(t.into());
292 }
293 AnyStrippedStateEvent::RoomPowerLevels(p) => {
294 self.max_power_level = p.power_levels().max().into();
295 }
296 AnyStrippedStateEvent::CallMember(_) => {
297 return false;
300 }
301 AnyStrippedStateEvent::RoomPinnedEvents(p) => {
302 if let Some(pinned) = p.content.pinned.clone() {
303 self.pinned_events = Some(RoomPinnedEventsEventContent::new(pinned));
304 }
305 }
306 _ => return false,
307 }
308
309 true
310 }
311
312 pub(super) fn handle_redaction(&mut self, redacts: &EventId) {
313 let room_version = self.room_version().unwrap_or(&ROOM_VERSION_FALLBACK).to_owned();
314
315 if self.avatar.has_event_id(redacts) {
317 self.avatar.as_mut().unwrap().redact(&room_version);
318 } else if self.canonical_alias.has_event_id(redacts) {
319 self.canonical_alias.as_mut().unwrap().redact(&room_version);
320 } else if self.create.has_event_id(redacts) {
321 self.create.as_mut().unwrap().redact(&room_version);
322 } else if self.guest_access.has_event_id(redacts) {
323 self.guest_access.as_mut().unwrap().redact(&room_version);
324 } else if self.history_visibility.has_event_id(redacts) {
325 self.history_visibility.as_mut().unwrap().redact(&room_version);
326 } else if self.join_rules.has_event_id(redacts) {
327 self.join_rules.as_mut().unwrap().redact(&room_version);
328 } else if self.name.has_event_id(redacts) {
329 self.name.as_mut().unwrap().redact(&room_version);
330 } else if self.tombstone.has_event_id(redacts) {
331 self.tombstone.as_mut().unwrap().redact(&room_version);
332 } else if self.topic.has_event_id(redacts) {
333 self.topic.as_mut().unwrap().redact(&room_version);
334 } else {
335 self.rtc_member_events
336 .retain(|_, member_event| member_event.event_id() != Some(redacts));
337 }
338 }
339
340 pub fn handle_notable_tags(&mut self, tags: &Tags) {
341 let mut notable_tags = RoomNotableTags::empty();
342
343 if tags.contains_key(&TagName::Favorite) {
344 notable_tags.insert(RoomNotableTags::FAVOURITE);
345 }
346
347 if tags.contains_key(&TagName::LowPriority) {
348 notable_tags.insert(RoomNotableTags::LOW_PRIORITY);
349 }
350
351 self.notable_tags = notable_tags;
352 }
353}
354
355impl Default for BaseRoomInfo {
356 fn default() -> Self {
357 Self {
358 avatar: None,
359 beacons: BTreeMap::new(),
360 canonical_alias: None,
361 create: None,
362 dm_targets: Default::default(),
363 encryption: None,
364 guest_access: None,
365 history_visibility: None,
366 join_rules: None,
367 max_power_level: 100,
368 name: None,
369 tombstone: None,
370 topic: None,
371 rtc_member_events: BTreeMap::new(),
372 is_marked_unread: false,
373 is_marked_unread_source: AccountDataSource::Unstable,
374 notable_tags: RoomNotableTags::empty(),
375 pinned_events: None,
376 }
377 }
378}
379
380trait OptionExt {
381 fn has_event_id(&self, ev_id: &EventId) -> bool;
382}
383
384impl<C> OptionExt for Option<MinimalStateEvent<C>>
385where
386 C: StaticStateEventContent + RedactContent,
387 C::Redacted: RedactedStateEventContent,
388{
389 fn has_event_id(&self, ev_id: &EventId) -> bool {
390 self.as_ref().is_some_and(|ev| ev.event_id() == Some(ev_id))
391 }
392}
393
394#[derive(Clone, Debug, Serialize, Deserialize)]
398pub struct RoomInfo {
399 #[serde(default, alias = "version")]
402 pub(crate) data_format_version: u8,
403
404 pub(crate) room_id: OwnedRoomId,
406
407 pub(crate) room_state: RoomState,
409
410 pub(crate) notification_counts: UnreadNotificationsCount,
415
416 pub(crate) summary: RoomSummary,
418
419 pub(crate) members_synced: bool,
421
422 pub(crate) last_prev_batch: Option<String>,
424
425 pub(crate) sync_info: SyncInfo,
427
428 pub(crate) encryption_state_synced: bool,
430
431 pub(crate) latest_event: Option<Box<LatestEvent>>,
433
434 #[serde(default)]
436 pub(crate) read_receipts: RoomReadReceipts,
437
438 pub(crate) base_info: Box<BaseRoomInfo>,
441
442 #[serde(skip)]
446 pub(crate) warned_about_unknown_room_version: Arc<AtomicBool>,
447
448 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub(crate) cached_display_name: Option<RoomDisplayName>,
454
455 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub(crate) cached_user_defined_notification_mode: Option<RoomNotificationMode>,
458
459 #[serde(default)]
466 pub(crate) recency_stamp: Option<u64>,
467
468 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub(crate) invite_accepted_at: Option<MilliSecondsSinceUnixEpoch>,
475}
476
477impl RoomInfo {
478 #[doc(hidden)] pub fn new(room_id: &RoomId, room_state: RoomState) -> Self {
480 Self {
481 data_format_version: 1,
482 room_id: room_id.into(),
483 room_state,
484 notification_counts: Default::default(),
485 summary: Default::default(),
486 members_synced: false,
487 last_prev_batch: None,
488 sync_info: SyncInfo::NoState,
489 encryption_state_synced: false,
490 latest_event: None,
491 read_receipts: Default::default(),
492 base_info: Box::new(BaseRoomInfo::new()),
493 warned_about_unknown_room_version: Arc::new(false.into()),
494 cached_display_name: None,
495 cached_user_defined_notification_mode: None,
496 recency_stamp: None,
497 invite_accepted_at: None,
498 }
499 }
500
501 pub fn mark_as_joined(&mut self) {
503 self.set_state(RoomState::Joined);
504 }
505
506 pub fn mark_as_left(&mut self) {
508 self.set_state(RoomState::Left);
509 }
510
511 pub fn mark_as_invited(&mut self) {
513 self.set_state(RoomState::Invited);
514 }
515
516 pub fn mark_as_knocked(&mut self) {
518 self.set_state(RoomState::Knocked);
519 }
520
521 pub fn mark_as_banned(&mut self) {
523 self.set_state(RoomState::Banned);
524 }
525
526 pub fn set_state(&mut self, room_state: RoomState) {
528 self.room_state = room_state;
529 }
530
531 pub fn mark_members_synced(&mut self) {
533 self.members_synced = true;
534 }
535
536 pub fn mark_members_missing(&mut self) {
538 self.members_synced = false;
539 }
540
541 pub fn are_members_synced(&self) -> bool {
543 self.members_synced
544 }
545
546 pub fn mark_state_partially_synced(&mut self) {
548 self.sync_info = SyncInfo::PartiallySynced;
549 }
550
551 pub fn mark_state_fully_synced(&mut self) {
553 self.sync_info = SyncInfo::FullySynced;
554 }
555
556 pub fn mark_state_not_synced(&mut self) {
558 self.sync_info = SyncInfo::NoState;
559 }
560
561 pub fn mark_encryption_state_synced(&mut self) {
563 self.encryption_state_synced = true;
564 }
565
566 pub fn mark_encryption_state_missing(&mut self) {
568 self.encryption_state_synced = false;
569 }
570
571 pub fn set_prev_batch(&mut self, prev_batch: Option<&str>) -> bool {
575 if self.last_prev_batch.as_deref() != prev_batch {
576 self.last_prev_batch = prev_batch.map(|p| p.to_owned());
577 true
578 } else {
579 false
580 }
581 }
582
583 pub fn state(&self) -> RoomState {
585 self.room_state
586 }
587
588 pub fn encryption_state(&self) -> EncryptionState {
590 if !self.encryption_state_synced {
591 EncryptionState::Unknown
592 } else if self.base_info.encryption.is_some() {
593 EncryptionState::Encrypted
594 } else {
595 EncryptionState::NotEncrypted
596 }
597 }
598
599 pub fn set_encryption_event(&mut self, event: Option<RoomEncryptionEventContent>) {
601 self.base_info.encryption = event;
602 }
603
604 pub fn handle_encryption_state(
606 &mut self,
607 requested_required_states: &[(StateEventType, String)],
608 ) {
609 if requested_required_states
610 .iter()
611 .any(|(state_event, _)| state_event == &StateEventType::RoomEncryption)
612 {
613 self.mark_encryption_state_synced();
619 }
620 }
621
622 pub fn handle_state_event(&mut self, event: &AnySyncStateEvent) -> bool {
626 let base_info_has_been_modified = self.base_info.handle_state_event(event);
628
629 if let AnySyncStateEvent::RoomEncryption(_) = event {
630 self.mark_encryption_state_synced();
636 }
637
638 base_info_has_been_modified
639 }
640
641 pub fn handle_stripped_state_event(&mut self, event: &AnyStrippedStateEvent) -> bool {
645 self.base_info.handle_stripped_state_event(event)
646 }
647
648 #[instrument(skip_all, fields(redacts))]
650 pub fn handle_redaction(
651 &mut self,
652 event: &SyncRoomRedactionEvent,
653 _raw: &Raw<SyncRoomRedactionEvent>,
654 ) {
655 let room_version = self.room_version_or_default();
656
657 let Some(redacts) = event.redacts(&room_version) else {
658 info!("Can't apply redaction, redacts field is missing");
659 return;
660 };
661 tracing::Span::current().record("redacts", debug(redacts));
662
663 if let Some(latest_event) = &mut self.latest_event {
664 tracing::trace!("Checking if redaction applies to latest event");
665 if latest_event.event_id().as_deref() == Some(redacts) {
666 match apply_redaction(latest_event.event().raw(), _raw, &room_version) {
667 Some(redacted) => {
668 latest_event.event_mut().kind =
671 TimelineEventKind::PlainText { event: redacted };
672 debug!("Redacted latest event");
673 }
674 None => {
675 self.latest_event = None;
676 debug!("Removed latest event");
677 }
678 }
679 }
680 }
681
682 self.base_info.handle_redaction(redacts);
683 }
684
685 pub fn avatar_url(&self) -> Option<&MxcUri> {
687 self.base_info
688 .avatar
689 .as_ref()
690 .and_then(|e| e.as_original().and_then(|e| e.content.url.as_deref()))
691 }
692
693 pub fn update_avatar(&mut self, url: Option<OwnedMxcUri>) {
695 self.base_info.avatar = url.map(|url| {
696 let mut content = RoomAvatarEventContent::new();
697 content.url = Some(url);
698
699 MinimalStateEvent::Original(OriginalMinimalStateEvent { content, event_id: None })
700 });
701 }
702
703 pub fn avatar_info(&self) -> Option<&avatar::ImageInfo> {
705 self.base_info
706 .avatar
707 .as_ref()
708 .and_then(|e| e.as_original().and_then(|e| e.content.info.as_deref()))
709 }
710
711 pub fn update_notification_count(&mut self, notification_counts: UnreadNotificationsCount) {
713 self.notification_counts = notification_counts;
714 }
715
716 pub fn update_from_ruma_summary(&mut self, summary: &RumaSummary) -> bool {
720 let mut changed = false;
721
722 if !summary.is_empty() {
723 if !summary.heroes.is_empty() {
724 self.summary.room_heroes = summary
725 .heroes
726 .iter()
727 .map(|hero_id| RoomHero {
728 user_id: hero_id.to_owned(),
729 display_name: None,
730 avatar_url: None,
731 })
732 .collect();
733
734 changed = true;
735 }
736
737 if let Some(joined) = summary.joined_member_count {
738 self.summary.joined_member_count = joined.into();
739 changed = true;
740 }
741
742 if let Some(invited) = summary.invited_member_count {
743 self.summary.invited_member_count = invited.into();
744 changed = true;
745 }
746 }
747
748 changed
749 }
750
751 pub(crate) fn update_joined_member_count(&mut self, count: u64) {
753 self.summary.joined_member_count = count;
754 }
755
756 pub(crate) fn update_invited_member_count(&mut self, count: u64) {
758 self.summary.invited_member_count = count;
759 }
760
761 pub(crate) fn set_invite_accepted_now(&mut self) {
764 self.invite_accepted_at = Some(MilliSecondsSinceUnixEpoch::now());
765 }
766
767 pub fn invite_accepted_at(&self) -> Option<MilliSecondsSinceUnixEpoch> {
774 self.invite_accepted_at
775 }
776
777 pub(crate) fn update_heroes(&mut self, heroes: Vec<RoomHero>) {
779 self.summary.room_heroes = heroes;
780 }
781
782 pub fn heroes(&self) -> &[RoomHero] {
784 &self.summary.room_heroes
785 }
786
787 pub fn active_members_count(&self) -> u64 {
791 self.summary.joined_member_count.saturating_add(self.summary.invited_member_count)
792 }
793
794 pub fn invited_members_count(&self) -> u64 {
796 self.summary.invited_member_count
797 }
798
799 pub fn joined_members_count(&self) -> u64 {
801 self.summary.joined_member_count
802 }
803
804 pub fn canonical_alias(&self) -> Option<&RoomAliasId> {
806 self.base_info.canonical_alias.as_ref()?.as_original()?.content.alias.as_deref()
807 }
808
809 pub fn alt_aliases(&self) -> &[OwnedRoomAliasId] {
811 self.base_info
812 .canonical_alias
813 .as_ref()
814 .and_then(|ev| ev.as_original())
815 .map(|ev| ev.content.alt_aliases.as_ref())
816 .unwrap_or_default()
817 }
818
819 pub fn room_id(&self) -> &RoomId {
821 &self.room_id
822 }
823
824 pub fn room_version(&self) -> Option<&RoomVersionId> {
826 self.base_info.room_version()
827 }
828
829 pub fn room_version_or_default(&self) -> RoomVersionId {
834 use std::sync::atomic::Ordering;
835
836 self.base_info.room_version().cloned().unwrap_or_else(|| {
837 if self
838 .warned_about_unknown_room_version
839 .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed)
840 .is_ok()
841 {
842 warn!("Unknown room version, falling back to {ROOM_VERSION_FALLBACK}");
843 }
844
845 ROOM_VERSION_FALLBACK
846 })
847 }
848
849 pub fn room_type(&self) -> Option<&RoomType> {
851 match self.base_info.create.as_ref()? {
852 MinimalStateEvent::Original(ev) => ev.content.room_type.as_ref(),
853 MinimalStateEvent::Redacted(ev) => ev.content.room_type.as_ref(),
854 }
855 }
856
857 pub fn creator(&self) -> Option<&UserId> {
859 match self.base_info.create.as_ref()? {
860 MinimalStateEvent::Original(ev) => Some(&ev.content.creator),
861 MinimalStateEvent::Redacted(ev) => Some(&ev.content.creator),
862 }
863 }
864
865 pub(super) fn guest_access(&self) -> &GuestAccess {
866 match &self.base_info.guest_access {
867 Some(MinimalStateEvent::Original(ev)) => &ev.content.guest_access,
868 _ => &GuestAccess::Forbidden,
869 }
870 }
871
872 pub fn history_visibility(&self) -> Option<&HistoryVisibility> {
876 match &self.base_info.history_visibility {
877 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.history_visibility),
878 _ => None,
879 }
880 }
881
882 pub fn history_visibility_or_default(&self) -> &HistoryVisibility {
889 match &self.base_info.history_visibility {
890 Some(MinimalStateEvent::Original(ev)) => &ev.content.history_visibility,
891 _ => &HistoryVisibility::Shared,
892 }
893 }
894
895 pub fn join_rule(&self) -> Option<&JoinRule> {
898 match &self.base_info.join_rules {
899 Some(MinimalStateEvent::Original(ev)) => Some(&ev.content.join_rule),
900 _ => None,
901 }
902 }
903
904 pub fn name(&self) -> Option<&str> {
906 let name = &self.base_info.name.as_ref()?.as_original()?.content.name;
907 (!name.is_empty()).then_some(name)
908 }
909
910 pub fn create(&self) -> Option<&RoomCreateWithCreatorEventContent> {
912 Some(&self.base_info.create.as_ref()?.as_original()?.content)
913 }
914
915 pub fn tombstone(&self) -> Option<&RoomTombstoneEventContent> {
917 Some(&self.base_info.tombstone.as_ref()?.as_original()?.content)
918 }
919
920 pub fn topic(&self) -> Option<&str> {
922 Some(&self.base_info.topic.as_ref()?.as_original()?.content.topic)
923 }
924
925 fn active_matrix_rtc_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
930 let mut v = self
931 .base_info
932 .rtc_member_events
933 .iter()
934 .filter_map(|(user_id, ev)| {
935 ev.as_original().map(|ev| {
936 ev.content
937 .active_memberships(None)
938 .into_iter()
939 .map(move |m| (user_id.clone(), m))
940 })
941 })
942 .flatten()
943 .collect::<Vec<_>>();
944 v.sort_by_key(|(_, m)| m.created_ts());
945 v
946 }
947
948 fn active_room_call_memberships(&self) -> Vec<(CallMemberStateKey, MembershipData<'_>)> {
954 self.active_matrix_rtc_memberships()
955 .into_iter()
956 .filter(|(_user_id, m)| m.is_room_call())
957 .collect()
958 }
959
960 pub fn has_active_room_call(&self) -> bool {
963 !self.active_room_call_memberships().is_empty()
964 }
965
966 pub fn active_room_call_participants(&self) -> Vec<OwnedUserId> {
975 self.active_room_call_memberships()
976 .iter()
977 .map(|(call_member_state_key, _)| call_member_state_key.user_id().to_owned())
978 .collect()
979 }
980
981 pub fn latest_event(&self) -> Option<&LatestEvent> {
983 self.latest_event.as_deref()
984 }
985
986 pub(crate) fn update_recency_stamp(&mut self, stamp: u64) {
990 self.recency_stamp = Some(stamp);
991 }
992
993 pub fn pinned_event_ids(&self) -> Option<Vec<OwnedEventId>> {
995 self.base_info.pinned_events.clone().map(|c| c.pinned)
996 }
997
998 pub fn is_pinned_event(&self, event_id: &EventId) -> bool {
1004 self.base_info
1005 .pinned_events
1006 .as_ref()
1007 .map(|p| p.pinned.contains(&event_id.to_owned()))
1008 .unwrap_or_default()
1009 }
1010
1011 #[instrument(skip_all, fields(room_id = ?self.room_id))]
1019 pub(crate) async fn apply_migrations(&mut self, store: Arc<DynStateStore>) -> bool {
1020 let mut migrated = false;
1021
1022 if self.data_format_version < 1 {
1023 info!("Migrating room info to version 1");
1024
1025 match store.get_room_account_data_event_static::<TagEventContent>(&self.room_id).await {
1027 Ok(Some(raw_event)) => match raw_event.deserialize() {
1029 Ok(event) => {
1030 self.base_info.handle_notable_tags(&event.content.tags);
1031 }
1032 Err(error) => {
1033 warn!("Failed to deserialize room tags: {error}");
1034 }
1035 },
1036 Ok(_) => {
1037 }
1039 Err(error) => {
1040 warn!("Failed to load room tags: {error}");
1041 }
1042 }
1043
1044 match store.get_state_event_static::<RoomPinnedEventsEventContent>(&self.room_id).await
1046 {
1047 Ok(Some(RawSyncOrStrippedState::Sync(raw_event))) => {
1049 match raw_event.deserialize() {
1050 Ok(event) => {
1051 self.handle_state_event(&event.into());
1052 }
1053 Err(error) => {
1054 warn!("Failed to deserialize room pinned events: {error}");
1055 }
1056 }
1057 }
1058 Ok(_) => {
1059 }
1061 Err(error) => {
1062 warn!("Failed to load room pinned events: {error}");
1063 }
1064 }
1065
1066 self.data_format_version = 1;
1067 migrated = true;
1068 }
1069
1070 migrated
1071 }
1072}
1073
1074#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
1075pub(crate) enum SyncInfo {
1076 NoState,
1082
1083 PartiallySynced,
1086
1087 FullySynced,
1089}
1090
1091pub fn apply_redaction(
1094 event: &Raw<AnySyncTimelineEvent>,
1095 raw_redaction: &Raw<SyncRoomRedactionEvent>,
1096 room_version: &RoomVersionId,
1097) -> Option<Raw<AnySyncTimelineEvent>> {
1098 use ruma::canonical_json::{redact_in_place, RedactedBecause};
1099
1100 let mut event_json = match event.deserialize_as() {
1101 Ok(json) => json,
1102 Err(e) => {
1103 warn!("Failed to deserialize latest event: {e}");
1104 return None;
1105 }
1106 };
1107
1108 let redacted_because = match RedactedBecause::from_raw_event(raw_redaction) {
1109 Ok(rb) => rb,
1110 Err(e) => {
1111 warn!("Redaction event is not valid canonical JSON: {e}");
1112 return None;
1113 }
1114 };
1115
1116 let redact_result = redact_in_place(&mut event_json, room_version, Some(redacted_because));
1117
1118 if let Err(e) = redact_result {
1119 warn!("Failed to redact event: {e}");
1120 return None;
1121 }
1122
1123 let raw = Raw::new(&event_json).expect("CanonicalJsonObject must be serializable");
1124 Some(raw.cast())
1125}
1126
1127#[derive(Debug, Clone)]
1137pub struct RoomInfoNotableUpdate {
1138 pub room_id: OwnedRoomId,
1140
1141 pub reasons: RoomInfoNotableUpdateReasons,
1143}
1144
1145bitflags! {
1146 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1148 pub struct RoomInfoNotableUpdateReasons: u8 {
1149 const RECENCY_STAMP = 0b0000_0001;
1151
1152 const LATEST_EVENT = 0b0000_0010;
1154
1155 const READ_RECEIPT = 0b0000_0100;
1157
1158 const UNREAD_MARKER = 0b0000_1000;
1160
1161 const MEMBERSHIP = 0b0001_0000;
1163
1164 const DISPLAY_NAME = 0b0010_0000;
1166
1167 const NONE = 0b1000_0000;
1178 }
1179}
1180
1181impl Default for RoomInfoNotableUpdateReasons {
1182 fn default() -> Self {
1183 Self::empty()
1184 }
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189 use std::sync::Arc;
1190
1191 use matrix_sdk_common::deserialized_responses::TimelineEvent;
1192 use matrix_sdk_test::{
1193 async_test,
1194 test_json::{sync_events::PINNED_EVENTS, TAG},
1195 };
1196 use ruma::{
1197 assign, events::room::pinned_events::RoomPinnedEventsEventContent, owned_event_id,
1198 owned_mxc_uri, owned_user_id, room_id, serde::Raw,
1199 };
1200 use serde_json::json;
1201 use similar_asserts::assert_eq;
1202
1203 use super::{BaseRoomInfo, RoomInfo, SyncInfo};
1204 use crate::{
1205 latest_event::LatestEvent,
1206 notification_settings::RoomNotificationMode,
1207 room::{RoomNotableTags, RoomSummary},
1208 store::{IntoStateStore, MemoryStore},
1209 sync::UnreadNotificationsCount,
1210 RoomDisplayName, RoomHero, RoomState, StateChanges,
1211 };
1212
1213 #[test]
1214 fn test_room_info_serialization() {
1215 let info = RoomInfo {
1219 data_format_version: 1,
1220 room_id: room_id!("!gda78o:server.tld").into(),
1221 room_state: RoomState::Invited,
1222 notification_counts: UnreadNotificationsCount {
1223 highlight_count: 1,
1224 notification_count: 2,
1225 },
1226 summary: RoomSummary {
1227 room_heroes: vec![RoomHero {
1228 user_id: owned_user_id!("@somebody:example.org"),
1229 display_name: None,
1230 avatar_url: None,
1231 }],
1232 joined_member_count: 5,
1233 invited_member_count: 0,
1234 },
1235 members_synced: true,
1236 last_prev_batch: Some("pb".to_owned()),
1237 sync_info: SyncInfo::FullySynced,
1238 encryption_state_synced: true,
1239 latest_event: Some(Box::new(LatestEvent::new(TimelineEvent::from_plaintext(
1240 Raw::from_json_string(json!({"sender": "@u:i.uk"}).to_string()).unwrap(),
1241 )))),
1242 base_info: Box::new(
1243 assign!(BaseRoomInfo::new(), { pinned_events: Some(RoomPinnedEventsEventContent::new(vec![owned_event_id!("$a")])) }),
1244 ),
1245 read_receipts: Default::default(),
1246 warned_about_unknown_room_version: Arc::new(false.into()),
1247 cached_display_name: None,
1248 cached_user_defined_notification_mode: None,
1249 recency_stamp: Some(42),
1250 invite_accepted_at: None,
1251 };
1252
1253 let info_json = json!({
1254 "data_format_version": 1,
1255 "room_id": "!gda78o:server.tld",
1256 "room_state": "Invited",
1257 "notification_counts": {
1258 "highlight_count": 1,
1259 "notification_count": 2,
1260 },
1261 "summary": {
1262 "room_heroes": [{
1263 "user_id": "@somebody:example.org",
1264 "display_name": null,
1265 "avatar_url": null
1266 }],
1267 "joined_member_count": 5,
1268 "invited_member_count": 0,
1269 },
1270 "members_synced": true,
1271 "last_prev_batch": "pb",
1272 "sync_info": "FullySynced",
1273 "encryption_state_synced": true,
1274 "latest_event": {
1275 "event": {
1276 "kind": {"PlainText": {"event": {"sender": "@u:i.uk"}}},
1277 "thread_summary": "None"
1278 },
1279 },
1280 "base_info": {
1281 "avatar": null,
1282 "canonical_alias": null,
1283 "create": null,
1284 "dm_targets": [],
1285 "encryption": null,
1286 "guest_access": null,
1287 "history_visibility": null,
1288 "is_marked_unread": false,
1289 "is_marked_unread_source": "Unstable",
1290 "join_rules": null,
1291 "max_power_level": 100,
1292 "name": null,
1293 "tombstone": null,
1294 "topic": null,
1295 "pinned_events": {
1296 "pinned": ["$a"]
1297 },
1298 },
1299 "read_receipts": {
1300 "num_unread": 0,
1301 "num_mentions": 0,
1302 "num_notifications": 0,
1303 "latest_active": null,
1304 "pending": [],
1305 },
1306 "recency_stamp": 42,
1307 });
1308
1309 assert_eq!(serde_json::to_value(info).unwrap(), info_json);
1310 }
1311
1312 #[async_test]
1313 async fn test_room_info_migration_v1() {
1314 let store = MemoryStore::new().into_state_store();
1315
1316 let room_info_json = json!({
1317 "room_id": "!gda78o:server.tld",
1318 "room_state": "Joined",
1319 "notification_counts": {
1320 "highlight_count": 1,
1321 "notification_count": 2,
1322 },
1323 "summary": {
1324 "room_heroes": [{
1325 "user_id": "@somebody:example.org",
1326 "display_name": null,
1327 "avatar_url": null
1328 }],
1329 "joined_member_count": 5,
1330 "invited_member_count": 0,
1331 },
1332 "members_synced": true,
1333 "last_prev_batch": "pb",
1334 "sync_info": "FullySynced",
1335 "encryption_state_synced": true,
1336 "latest_event": {
1337 "event": {
1338 "encryption_info": null,
1339 "event": {
1340 "sender": "@u:i.uk",
1341 },
1342 },
1343 },
1344 "base_info": {
1345 "avatar": null,
1346 "canonical_alias": null,
1347 "create": null,
1348 "dm_targets": [],
1349 "encryption": null,
1350 "guest_access": null,
1351 "history_visibility": null,
1352 "join_rules": null,
1353 "max_power_level": 100,
1354 "name": null,
1355 "tombstone": null,
1356 "topic": null,
1357 },
1358 "read_receipts": {
1359 "num_unread": 0,
1360 "num_mentions": 0,
1361 "num_notifications": 0,
1362 "latest_active": null,
1363 "pending": []
1364 },
1365 "recency_stamp": 42,
1366 });
1367 let mut room_info: RoomInfo = serde_json::from_value(room_info_json).unwrap();
1368
1369 assert_eq!(room_info.data_format_version, 0);
1370 assert!(room_info.base_info.notable_tags.is_empty());
1371 assert!(room_info.base_info.pinned_events.is_none());
1372
1373 assert!(room_info.apply_migrations(store.clone()).await);
1375
1376 assert_eq!(room_info.data_format_version, 1);
1377 assert!(room_info.base_info.notable_tags.is_empty());
1378 assert!(room_info.base_info.pinned_events.is_none());
1379
1380 assert!(!room_info.apply_migrations(store.clone()).await);
1382
1383 assert_eq!(room_info.data_format_version, 1);
1384 assert!(room_info.base_info.notable_tags.is_empty());
1385 assert!(room_info.base_info.pinned_events.is_none());
1386
1387 let mut changes = StateChanges::default();
1389
1390 let raw_tag_event = Raw::new(&*TAG).unwrap().cast();
1391 let tag_event = raw_tag_event.deserialize().unwrap();
1392 changes.add_room_account_data(&room_info.room_id, tag_event, raw_tag_event);
1393
1394 let raw_pinned_events_event = Raw::new(&*PINNED_EVENTS).unwrap().cast();
1395 let pinned_events_event = raw_pinned_events_event.deserialize().unwrap();
1396 changes.add_state_event(&room_info.room_id, pinned_events_event, raw_pinned_events_event);
1397
1398 store.save_changes(&changes).await.unwrap();
1399
1400 room_info.data_format_version = 0;
1402 assert!(room_info.apply_migrations(store.clone()).await);
1403
1404 assert_eq!(room_info.data_format_version, 1);
1405 assert!(room_info.base_info.notable_tags.contains(RoomNotableTags::FAVOURITE));
1406 assert!(room_info.base_info.pinned_events.is_some());
1407
1408 let new_room_info = RoomInfo::new(room_id!("!new_room:localhost"), RoomState::Joined);
1410 assert_eq!(new_room_info.data_format_version, 1);
1411 }
1412
1413 #[test]
1414 fn test_room_info_deserialization() {
1415 let info_json = json!({
1416 "room_id": "!gda78o:server.tld",
1417 "room_state": "Joined",
1418 "notification_counts": {
1419 "highlight_count": 1,
1420 "notification_count": 2,
1421 },
1422 "summary": {
1423 "room_heroes": [{
1424 "user_id": "@somebody:example.org",
1425 "display_name": "Somebody",
1426 "avatar_url": "mxc://example.org/abc"
1427 }],
1428 "joined_member_count": 5,
1429 "invited_member_count": 0,
1430 },
1431 "members_synced": true,
1432 "last_prev_batch": "pb",
1433 "sync_info": "FullySynced",
1434 "encryption_state_synced": true,
1435 "base_info": {
1436 "avatar": null,
1437 "canonical_alias": null,
1438 "create": null,
1439 "dm_targets": [],
1440 "encryption": null,
1441 "guest_access": null,
1442 "history_visibility": null,
1443 "join_rules": null,
1444 "max_power_level": 100,
1445 "name": null,
1446 "tombstone": null,
1447 "topic": null,
1448 },
1449 "cached_display_name": { "Calculated": "lol" },
1450 "cached_user_defined_notification_mode": "Mute",
1451 "recency_stamp": 42,
1452 });
1453
1454 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1455
1456 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1457 assert_eq!(info.room_state, RoomState::Joined);
1458 assert_eq!(info.notification_counts.highlight_count, 1);
1459 assert_eq!(info.notification_counts.notification_count, 2);
1460 assert_eq!(
1461 info.summary.room_heroes,
1462 vec![RoomHero {
1463 user_id: owned_user_id!("@somebody:example.org"),
1464 display_name: Some("Somebody".to_owned()),
1465 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1466 }]
1467 );
1468 assert_eq!(info.summary.joined_member_count, 5);
1469 assert_eq!(info.summary.invited_member_count, 0);
1470 assert!(info.members_synced);
1471 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1472 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1473 assert!(info.encryption_state_synced);
1474 assert!(info.latest_event.is_none());
1475 assert!(info.base_info.avatar.is_none());
1476 assert!(info.base_info.canonical_alias.is_none());
1477 assert!(info.base_info.create.is_none());
1478 assert_eq!(info.base_info.dm_targets.len(), 0);
1479 assert!(info.base_info.encryption.is_none());
1480 assert!(info.base_info.guest_access.is_none());
1481 assert!(info.base_info.history_visibility.is_none());
1482 assert!(info.base_info.join_rules.is_none());
1483 assert_eq!(info.base_info.max_power_level, 100);
1484 assert!(info.base_info.name.is_none());
1485 assert!(info.base_info.tombstone.is_none());
1486 assert!(info.base_info.topic.is_none());
1487
1488 assert_eq!(
1489 info.cached_display_name.as_ref(),
1490 Some(&RoomDisplayName::Calculated("lol".to_owned())),
1491 );
1492 assert_eq!(
1493 info.cached_user_defined_notification_mode.as_ref(),
1494 Some(&RoomNotificationMode::Mute)
1495 );
1496 assert_eq!(info.recency_stamp.as_ref(), Some(&42));
1497 }
1498
1499 #[test]
1506 fn test_room_info_deserialization_without_optional_items() {
1507 let info_json = json!({
1510 "room_id": "!gda78o:server.tld",
1511 "room_state": "Invited",
1512 "notification_counts": {
1513 "highlight_count": 1,
1514 "notification_count": 2,
1515 },
1516 "summary": {
1517 "room_heroes": [{
1518 "user_id": "@somebody:example.org",
1519 "display_name": "Somebody",
1520 "avatar_url": "mxc://example.org/abc"
1521 }],
1522 "joined_member_count": 5,
1523 "invited_member_count": 0,
1524 },
1525 "members_synced": true,
1526 "last_prev_batch": "pb",
1527 "sync_info": "FullySynced",
1528 "encryption_state_synced": true,
1529 "base_info": {
1530 "avatar": null,
1531 "canonical_alias": null,
1532 "create": null,
1533 "dm_targets": [],
1534 "encryption": null,
1535 "guest_access": null,
1536 "history_visibility": null,
1537 "join_rules": null,
1538 "max_power_level": 100,
1539 "name": null,
1540 "tombstone": null,
1541 "topic": null,
1542 },
1543 });
1544
1545 let info: RoomInfo = serde_json::from_value(info_json).unwrap();
1546
1547 assert_eq!(info.room_id, room_id!("!gda78o:server.tld"));
1548 assert_eq!(info.room_state, RoomState::Invited);
1549 assert_eq!(info.notification_counts.highlight_count, 1);
1550 assert_eq!(info.notification_counts.notification_count, 2);
1551 assert_eq!(
1552 info.summary.room_heroes,
1553 vec![RoomHero {
1554 user_id: owned_user_id!("@somebody:example.org"),
1555 display_name: Some("Somebody".to_owned()),
1556 avatar_url: Some(owned_mxc_uri!("mxc://example.org/abc")),
1557 }]
1558 );
1559 assert_eq!(info.summary.joined_member_count, 5);
1560 assert_eq!(info.summary.invited_member_count, 0);
1561 assert!(info.members_synced);
1562 assert_eq!(info.last_prev_batch, Some("pb".to_owned()));
1563 assert_eq!(info.sync_info, SyncInfo::FullySynced);
1564 assert!(info.encryption_state_synced);
1565 assert!(info.base_info.avatar.is_none());
1566 assert!(info.base_info.canonical_alias.is_none());
1567 assert!(info.base_info.create.is_none());
1568 assert_eq!(info.base_info.dm_targets.len(), 0);
1569 assert!(info.base_info.encryption.is_none());
1570 assert!(info.base_info.guest_access.is_none());
1571 assert!(info.base_info.history_visibility.is_none());
1572 assert!(info.base_info.join_rules.is_none());
1573 assert_eq!(info.base_info.max_power_level, 100);
1574 assert!(info.base_info.name.is_none());
1575 assert!(info.base_info.tombstone.is_none());
1576 assert!(info.base_info.topic.is_none());
1577 }
1578}