1use std::sync::Arc;
16
17use as_variant::as_variant;
18use matrix_sdk::{Room, deserialized_responses::TimelineEvent};
19use matrix_sdk_base::crypto::types::events::UtdCause;
20use ruma::{
21 OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId,
22 events::{
23 AnyMessageLikeEventContent, AnyStateEventContentChange, Mentions, MessageLikeEventType,
24 StateEventContentChange, StateEventType,
25 policy::rule::{
26 room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent,
27 user::PolicyRuleUserEventContent,
28 },
29 relation::Replacement,
30 room::{
31 avatar::RoomAvatarEventContent,
32 canonical_alias::RoomCanonicalAliasEventContent,
33 create::RoomCreateEventContent,
34 encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent},
35 encryption::RoomEncryptionEventContent,
36 guest_access::RoomGuestAccessEventContent,
37 history_visibility::RoomHistoryVisibilityEventContent,
38 join_rules::RoomJoinRulesEventContent,
39 member::{Change, RoomMemberEventContent},
40 message::{MessageType, RoomMessageEventContent},
41 name::RoomNameEventContent,
42 pinned_events::RoomPinnedEventsEventContent,
43 power_levels::RoomPowerLevelsEventContent,
44 server_acl::RoomServerAclEventContent,
45 third_party_invite::RoomThirdPartyInviteEventContent,
46 tombstone::RoomTombstoneEventContent,
47 topic::RoomTopicEventContent,
48 },
49 rtc::notification::CallIntent,
50 space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
51 sticker::StickerEventContent,
52 },
53 html::RemoveReplyFallback,
54 room_version_rules::RedactionRules,
55};
56use tracing::warn;
57
58mod live_location;
59mod message;
60mod msg_like;
61pub(super) mod other;
62pub(crate) mod pinned_events;
63mod polls;
64mod reply;
65
66pub use pinned_events::RoomPinnedEventsChange;
67
68pub(in crate::timeline) use self::{
69 live_location::beacon_info_matches,
70 message::{
71 extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
72 },
73};
74pub use self::{
75 live_location::{BeaconInfo, LiveLocationState},
76 message::Message,
77 msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary},
78 other::OtherMessageLike,
79 polls::{PollResult, PollState},
80 reply::{EmbeddedEvent, InReplyToDetails},
81};
82use super::ReactionsByKeyBySender;
83use crate::timeline::{
84 controller::ActiveCallInfo,
85 event_handler::{HandleAggregationKind, TimelineAction},
86};
87
88#[allow(clippy::large_enum_variant)]
90#[derive(Clone, Debug)]
91pub enum TimelineItemContent {
92 MsgLike(MsgLikeContent),
93
94 MembershipChange(RoomMembershipChange),
96
97 ProfileChange(MemberProfileChange),
99
100 OtherState(OtherState),
102
103 FailedToParseMessageLike {
105 event_type: MessageLikeEventType,
107
108 error: Arc<serde_json::Error>,
110 },
111
112 FailedToParseState {
114 event_type: StateEventType,
116
117 state_key: String,
119
120 error: Arc<serde_json::Error>,
122 },
123
124 CallInvite,
126
127 RtcNotification {
129 call_intent: Option<CallIntent>,
131 declined_by: Vec<OwnedUserId>,
133 active_call_info: Option<ActiveCallInfo>,
136 },
137}
138
139impl TimelineItemContent {
140 pub fn event_type_str(&self) -> Option<String> {
144 match self {
145 Self::MsgLike(msg) => Some(match &msg.kind {
146 MsgLikeKind::Message(_) => MessageLikeEventType::RoomMessage.to_string(),
147 MsgLikeKind::Sticker(_) => MessageLikeEventType::Sticker.to_string(),
148 MsgLikeKind::Poll(_) => MessageLikeEventType::PollStart.to_string(),
149 MsgLikeKind::Redacted => return None,
150 MsgLikeKind::UnableToDecrypt(_) => MessageLikeEventType::RoomEncrypted.to_string(),
151 MsgLikeKind::Other(other) => other.event_type().to_string(),
152 MsgLikeKind::LiveLocation(_) => StateEventType::BeaconInfo.to_string(),
153 }),
154 Self::MembershipChange(_) | Self::ProfileChange(_) => {
155 Some(StateEventType::RoomMember.to_string())
156 }
157 Self::OtherState(state) => Some(state.content().event_type().to_string()),
158 Self::FailedToParseMessageLike { event_type, .. } => Some(event_type.to_string()),
159 Self::FailedToParseState { event_type, .. } => Some(event_type.to_string()),
160 Self::CallInvite => Some(MessageLikeEventType::CallInvite.to_string()),
161 Self::RtcNotification { .. } => Some(MessageLikeEventType::RtcNotification.to_string()),
162 }
163 }
164
165 pub async fn from_event(room: &Room, timeline_event: TimelineEvent) -> Option<Self> {
169 let raw_event = timeline_event.into_raw();
170 let deserialized_event = raw_event.deserialize().ok()?;
171
172 let actions = TimelineAction::from_event(
173 deserialized_event,
174 &raw_event,
175 room,
176 None,
177 None,
178 None,
179 None,
180 )
181 .await;
182 match actions.as_slice() {
183 [TimelineAction::AddItem { content }] => Some(content.clone()),
184 [
186 TimelineAction::HandleAggregation {
187 kind: HandleAggregationKind::BeaconStop { content, .. },
188 ..
189 },
190 ] => Some(TimelineItemContent::MsgLike(MsgLikeContent {
191 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(content.clone())),
192 reactions: Default::default(),
193 thread_root: None,
194 in_reply_to: None,
195 thread_summary: None,
196 })),
197 [
198 TimelineAction::HandleAggregation {
199 kind:
200 HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
201 ..
202 },
203 ] => {
204 match TimelineAction::from_content(
206 AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
207 new_content.msgtype.clone(),
208 )),
209 None,
210 None,
211 None,
212 ) {
213 TimelineAction::AddItem { content } => Some(content),
214 _ => None,
215 }
216 }
217 [] => {
218 warn!("No action for event content processing");
219 None
220 }
221 [_, _, ..] => {
222 warn!("Ignoring event that produced multiple timeline actions");
226 None
227 }
228 _ => None,
229 }
230 }
231
232 pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
233 as_variant!(self, TimelineItemContent::MsgLike)
234 }
235
236 pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
240 as_variant!(self, Self::MsgLike(MsgLikeContent {
241 kind: MsgLikeKind::LiveLocation(state),
242 ..
243 }) => state)
244 }
245
246 pub fn as_message(&self) -> Option<&Message> {
249 as_variant!(self, Self::MsgLike(MsgLikeContent {
250 kind: MsgLikeKind::Message(message),
251 ..
252 }) => message)
253 }
254
255 pub fn is_message(&self) -> bool {
258 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
259 }
260
261 pub fn as_poll(&self) -> Option<&PollState> {
264 as_variant!(self, Self::MsgLike(MsgLikeContent {
265 kind: MsgLikeKind::Poll(poll_state),
266 ..
267 }) => poll_state)
268 }
269
270 pub fn is_poll(&self) -> bool {
273 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
274 }
275
276 pub fn as_sticker(&self) -> Option<&Sticker> {
277 as_variant!(
278 self,
279 Self::MsgLike(MsgLikeContent {
280 kind: MsgLikeKind::Sticker(sticker),
281 ..
282 }) => sticker
283 )
284 }
285
286 pub fn is_sticker(&self) -> bool {
289 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
290 }
291
292 pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
295 as_variant!(
296 self,
297 Self::MsgLike(MsgLikeContent {
298 kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
299 ..
300 }) => encrypted_message
301 )
302 }
303
304 pub fn is_unable_to_decrypt(&self) -> bool {
307 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
308 }
309
310 pub fn is_redacted(&self) -> bool {
311 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
312 }
313
314 pub(crate) fn message(
317 msgtype: MessageType,
318 mentions: Option<Mentions>,
319 reactions: ReactionsByKeyBySender,
320 thread_root: Option<OwnedEventId>,
321 in_reply_to: Option<InReplyToDetails>,
322 thread_summary: Option<ThreadSummary>,
323 ) -> Self {
324 let remove_reply_fallback =
325 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
326
327 Self::MsgLike(MsgLikeContent {
328 kind: MsgLikeKind::Message(Message::from_event(
329 msgtype,
330 mentions,
331 None,
332 remove_reply_fallback,
333 )),
334 reactions,
335 thread_root,
336 in_reply_to,
337 thread_summary,
338 })
339 }
340
341 #[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
343 match self {
344 TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
345 TimelineItemContent::MembershipChange(_) => "a membership change",
346 TimelineItemContent::ProfileChange(_) => "a profile change",
347 TimelineItemContent::OtherState(_) => "a state event",
348 TimelineItemContent::FailedToParseMessageLike { .. }
349 | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
350 TimelineItemContent::CallInvite => "a call invite",
351 TimelineItemContent::RtcNotification { .. } => "a call notification",
352 }
353 }
354
355 pub(crate) fn room_member(
356 user_id: OwnedUserId,
357 full_content: StateEventContentChange<RoomMemberEventContent>,
358 sender: OwnedUserId,
359 ) -> Self {
360 use ruma::events::room::member::MembershipChange as MChange;
361 match &full_content {
362 StateEventContentChange::Original { content, prev_content } => {
363 let membership_change = content.membership_change(
364 prev_content.as_ref().map(|c| c.details()),
365 &sender,
366 &user_id,
367 );
368
369 if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
370 membership_change
371 {
372 Self::ProfileChange(MemberProfileChange {
373 user_id,
374 displayname_change: displayname_change.map(|c| Change {
375 new: c.new.map(ToOwned::to_owned),
376 old: c.old.map(ToOwned::to_owned),
377 }),
378 avatar_url_change: avatar_url_change.map(|c| Change {
379 new: c.new.map(ToOwned::to_owned),
380 old: c.old.map(ToOwned::to_owned),
381 }),
382 })
383 } else {
384 let change = match membership_change {
385 MChange::None => MembershipChange::None,
386 MChange::Error => MembershipChange::Error,
387 MChange::Joined => MembershipChange::Joined,
388 MChange::Left => MembershipChange::Left,
389 MChange::Banned => MembershipChange::Banned,
390 MChange::Unbanned => MembershipChange::Unbanned,
391 MChange::Kicked => MembershipChange::Kicked,
392 MChange::Invited => MembershipChange::Invited,
393 MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
394 MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
395 MChange::InvitationRejected => MembershipChange::InvitationRejected,
396 MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
397 MChange::Knocked => MembershipChange::Knocked,
398 MChange::KnockAccepted => MembershipChange::KnockAccepted,
399 MChange::KnockRetracted => MembershipChange::KnockRetracted,
400 MChange::KnockDenied => MembershipChange::KnockDenied,
401 MChange::ProfileChanged { .. } => unreachable!(),
402 _ => MembershipChange::NotImplemented,
403 };
404
405 Self::MembershipChange(RoomMembershipChange {
406 user_id,
407 content: full_content,
408 change: Some(change),
409 })
410 }
411 }
412 StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
413 user_id,
414 content: full_content,
415 change: None,
416 }),
417 }
418 }
419
420 pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
421 match self {
422 Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification { .. } => {
423 TimelineItemContent::MsgLike(MsgLikeContent::redacted())
424 }
425 Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
426 Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
427 Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
428 Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
429 }
430 }
431
432 pub fn thread_root(&self) -> Option<OwnedEventId> {
434 as_variant!(self, Self::MsgLike)?.thread_root.clone()
435 }
436
437 pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
439 as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
440 }
441
442 pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
445 match self {
446 TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
447
448 TimelineItemContent::MembershipChange(..)
449 | TimelineItemContent::ProfileChange(..)
450 | TimelineItemContent::OtherState(..)
451 | TimelineItemContent::FailedToParseMessageLike { .. }
452 | TimelineItemContent::FailedToParseState { .. }
453 | TimelineItemContent::CallInvite
454 | TimelineItemContent::RtcNotification { .. } => {
455 None
457 }
458 }
459 }
460
461 pub fn thread_summary(&self) -> Option<ThreadSummary> {
463 as_variant!(self, Self::MsgLike)?.thread_summary.clone()
464 }
465
466 pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
470 match self {
471 TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
472
473 TimelineItemContent::MembershipChange(..)
474 | TimelineItemContent::ProfileChange(..)
475 | TimelineItemContent::OtherState(..)
476 | TimelineItemContent::FailedToParseMessageLike { .. }
477 | TimelineItemContent::FailedToParseState { .. }
478 | TimelineItemContent::CallInvite
479 | TimelineItemContent::RtcNotification { .. } => {
480 None
482 }
483 }
484 }
485
486 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
487 let mut cloned = self.clone();
488 if let Some(r) = cloned.reactions_mut() {
489 *r = reactions;
490 }
491 cloned
492 }
493}
494
495#[derive(Clone, Debug)]
497pub enum EncryptedMessage {
498 OlmV1Curve25519AesSha2 {
501 sender_key: String,
503 },
504 MegolmV1AesSha2 {
506 #[deprecated = "this field should still be sent but should not be used when received"]
508 #[doc(hidden)] sender_key: Option<String>,
510
511 #[deprecated = "this field should still be sent but should not be used when received"]
513 #[doc(hidden)] device_id: Option<OwnedDeviceId>,
515
516 session_id: String,
518
519 cause: UtdCause,
522 },
523 Unknown,
525}
526
527impl EncryptedMessage {
528 pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
529 match content.scheme {
530 EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
531 Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
532 }
533 #[allow(deprecated)]
534 EncryptedEventScheme::MegolmV1AesSha2(s) => {
535 let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
536
537 Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
538 }
539 _ => Self::Unknown,
540 }
541 }
542
543 pub(crate) fn session_id(&self) -> Option<&str> {
546 match self {
547 EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
548 EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
549 EncryptedMessage::Unknown => None,
550 }
551 }
552}
553
554#[derive(Clone, Debug)]
556pub struct Sticker {
557 pub(in crate::timeline) content: StickerEventContent,
558}
559
560impl Sticker {
561 pub fn content(&self) -> &StickerEventContent {
563 &self.content
564 }
565}
566
567#[derive(Clone, Debug)]
569pub struct RoomMembershipChange {
570 pub(in crate::timeline) user_id: OwnedUserId,
571 pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
572 pub(in crate::timeline) change: Option<MembershipChange>,
573}
574
575impl RoomMembershipChange {
576 pub fn user_id(&self) -> &UserId {
578 &self.user_id
579 }
580
581 pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
583 &self.content
584 }
585
586 pub fn display_name(&self) -> Option<String> {
589 if let StateEventContentChange::Original { content, prev_content } = &self.content {
590 content
591 .displayname
592 .as_ref()
593 .or_else(|| {
594 prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
595 })
596 .cloned()
597 } else {
598 None
599 }
600 }
601
602 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
605 if let StateEventContentChange::Original { content, prev_content } = &self.content {
606 content
607 .avatar_url
608 .as_ref()
609 .or_else(|| {
610 prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
611 })
612 .cloned()
613 } else {
614 None
615 }
616 }
617
618 pub fn change(&self) -> Option<MembershipChange> {
626 self.change
627 }
628
629 fn redact(&self, rules: &RedactionRules) -> Self {
630 Self {
631 user_id: self.user_id.clone(),
632 content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
633 change: self.change,
634 }
635 }
636}
637
638#[derive(Clone, Copy, Debug, PartialEq, Eq)]
640pub enum MembershipChange {
641 None,
643
644 Error,
646
647 Joined,
649
650 Left,
652
653 Banned,
655
656 Unbanned,
658
659 Kicked,
661
662 Invited,
664
665 KickedAndBanned,
667
668 InvitationAccepted,
670
671 InvitationRejected,
673
674 InvitationRevoked,
676
677 Knocked,
679
680 KnockAccepted,
682
683 KnockRetracted,
685
686 KnockDenied,
688
689 NotImplemented,
691}
692
693#[derive(Clone, Debug)]
698pub struct MemberProfileChange {
699 pub(in crate::timeline) user_id: OwnedUserId,
700 pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
701 pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
702}
703
704impl MemberProfileChange {
705 pub fn user_id(&self) -> &UserId {
707 &self.user_id
708 }
709
710 pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
712 self.displayname_change.as_ref()
713 }
714
715 pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
717 self.avatar_url_change.as_ref()
718 }
719
720 fn redact(&self) -> Self {
721 Self {
722 user_id: self.user_id.clone(),
723 displayname_change: None,
728 avatar_url_change: None,
729 }
730 }
731}
732
733#[derive(Clone, Debug)]
736pub enum AnyOtherStateEventContentChange {
737 PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
739
740 PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
742
743 PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
745
746 RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
748
749 RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
751
752 RoomCreate(StateEventContentChange<RoomCreateEventContent>),
754
755 RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
757
758 RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
760
761 RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
763
764 RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
766
767 RoomName(StateEventContentChange<RoomNameEventContent>),
769
770 RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
772
773 RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
775
776 RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
778
779 RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
781
782 RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
784
785 RoomTopic(StateEventContentChange<RoomTopicEventContent>),
787
788 SpaceChild(StateEventContentChange<SpaceChildEventContent>),
790
791 SpaceParent(StateEventContentChange<SpaceParentEventContent>),
793
794 #[doc(hidden)]
795 _Custom { event_type: String },
796}
797
798impl AnyOtherStateEventContentChange {
799 pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
805 let event_type = content.event_type();
806
807 match content {
808 AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
809 AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
810 AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
811 AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
812 AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
813 AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
814 AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
815 AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
816 AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
817 AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
818 AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
819 AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
820 AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
821 AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
822 AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
823 AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
824 AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
825 AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
826 AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
827 AnyStateEventContentChange::RoomMember(_) => unreachable!(),
828 _ => Self::_Custom { event_type: event_type.to_string() },
829 }
830 }
831
832 pub fn event_type(&self) -> StateEventType {
834 match self {
835 Self::PolicyRuleRoom(c) => c.event_type(),
836 Self::PolicyRuleServer(c) => c.event_type(),
837 Self::PolicyRuleUser(c) => c.event_type(),
838 Self::RoomAvatar(c) => c.event_type(),
839 Self::RoomCanonicalAlias(c) => c.event_type(),
840 Self::RoomCreate(c) => c.event_type(),
841 Self::RoomEncryption(c) => c.event_type(),
842 Self::RoomGuestAccess(c) => c.event_type(),
843 Self::RoomHistoryVisibility(c) => c.event_type(),
844 Self::RoomJoinRules(c) => c.event_type(),
845 Self::RoomName(c) => c.event_type(),
846 Self::RoomPinnedEvents(c) => c.event_type(),
847 Self::RoomPowerLevels(c) => c.event_type(),
848 Self::RoomServerAcl(c) => c.event_type(),
849 Self::RoomThirdPartyInvite(c) => c.event_type(),
850 Self::RoomTombstone(c) => c.event_type(),
851 Self::RoomTopic(c) => c.event_type(),
852 Self::SpaceChild(c) => c.event_type(),
853 Self::SpaceParent(c) => c.event_type(),
854 Self::_Custom { event_type } => event_type.as_str().into(),
855 }
856 }
857
858 fn redact(&self, rules: &RedactionRules) -> Self {
859 match self {
860 Self::PolicyRuleRoom(c) => {
861 Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
862 }
863 Self::PolicyRuleServer(c) => {
864 Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
865 }
866 Self::PolicyRuleUser(c) => {
867 Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
868 }
869 Self::RoomAvatar(c) => {
870 Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
871 }
872 Self::RoomCanonicalAlias(c) => {
873 Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
874 }
875 Self::RoomCreate(c) => {
876 Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
877 }
878 Self::RoomEncryption(c) => {
879 Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
880 }
881 Self::RoomGuestAccess(c) => {
882 Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
883 }
884 Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
885 StateEventContentChange::Redacted(c.clone().redact(rules)),
886 ),
887 Self::RoomJoinRules(c) => {
888 Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
889 }
890 Self::RoomName(c) => {
891 Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
892 }
893 Self::RoomPinnedEvents(c) => {
894 Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
895 }
896 Self::RoomPowerLevels(c) => {
897 Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
898 }
899 Self::RoomServerAcl(c) => {
900 Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
901 }
902 Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
903 StateEventContentChange::Redacted(c.clone().redact(rules)),
904 ),
905 Self::RoomTombstone(c) => {
906 Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
907 }
908 Self::RoomTopic(c) => {
909 Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
910 }
911 Self::SpaceChild(c) => {
912 Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
913 }
914 Self::SpaceParent(c) => {
915 Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
916 }
917 Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
918 }
919 }
920}
921
922#[derive(Clone, Debug)]
924pub struct OtherState {
925 pub(in crate::timeline) state_key: String,
926 pub(in crate::timeline) content: AnyOtherStateEventContentChange,
927}
928
929impl OtherState {
930 pub fn state_key(&self) -> &str {
932 &self.state_key
933 }
934
935 pub fn content(&self) -> &AnyOtherStateEventContentChange {
937 &self.content
938 }
939
940 fn redact(&self, rules: &RedactionRules) -> Self {
941 Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
942 }
943}
944
945#[cfg(test)]
946mod tests {
947 use assert_matches2::assert_let;
948 use matrix_sdk_test::ALICE;
949 use ruma::{
950 assign,
951 events::{
952 StateEventContentChange,
953 room::member::{
954 MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
955 },
956 },
957 room_version_rules::RedactionRules,
958 };
959
960 use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
961
962 #[test]
963 fn redact_membership_change() {
964 let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
965 user_id: ALICE.to_owned(),
966 content: StateEventContentChange::Original {
967 content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
968 reason: Some("🤬".to_owned()),
969 }),
970 prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
971 MembershipState::Join,
972 )),
973 },
974 change: Some(MembershipChange::Banned),
975 });
976
977 let redacted = content.redact(&RedactionRules::V11);
978 assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
979 assert_eq!(inner.change, Some(MembershipChange::Banned));
980 assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
981 assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
982 }
983}