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};
56
57mod live_location;
58mod message;
59mod msg_like;
60pub(super) mod other;
61pub(crate) mod pinned_events;
62mod polls;
63mod reply;
64
65pub use pinned_events::RoomPinnedEventsChange;
66
67pub(in crate::timeline) use self::{
68 live_location::beacon_info_matches,
69 message::{
70 extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
71 },
72};
73pub use self::{
74 live_location::{BeaconInfo, LiveLocationState},
75 message::Message,
76 msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary},
77 other::OtherMessageLike,
78 polls::{PollResult, PollState},
79 reply::{EmbeddedEvent, InReplyToDetails},
80};
81use super::ReactionsByKeyBySender;
82use crate::timeline::event_handler::{HandleAggregationKind, TimelineAction};
83
84#[allow(clippy::large_enum_variant)]
86#[derive(Clone, Debug)]
87pub enum TimelineItemContent {
88 MsgLike(MsgLikeContent),
89
90 MembershipChange(RoomMembershipChange),
92
93 ProfileChange(MemberProfileChange),
95
96 OtherState(OtherState),
98
99 FailedToParseMessageLike {
101 event_type: MessageLikeEventType,
103
104 error: Arc<serde_json::Error>,
106 },
107
108 FailedToParseState {
110 event_type: StateEventType,
112
113 state_key: String,
115
116 error: Arc<serde_json::Error>,
118 },
119
120 CallInvite,
122
123 RtcNotification {
125 call_intent: Option<CallIntent>,
127 declined_by: Vec<OwnedUserId>,
129 },
130}
131
132impl TimelineItemContent {
133 pub fn event_type_str(&self) -> Option<String> {
137 match self {
138 Self::MsgLike(msg) => Some(match &msg.kind {
139 MsgLikeKind::Message(_) => MessageLikeEventType::RoomMessage.to_string(),
140 MsgLikeKind::Sticker(_) => MessageLikeEventType::Sticker.to_string(),
141 MsgLikeKind::Poll(_) => MessageLikeEventType::PollStart.to_string(),
142 MsgLikeKind::Redacted => return None,
143 MsgLikeKind::UnableToDecrypt(_) => MessageLikeEventType::RoomEncrypted.to_string(),
144 MsgLikeKind::Other(other) => other.event_type().to_string(),
145 MsgLikeKind::LiveLocation(_) => StateEventType::BeaconInfo.to_string(),
146 }),
147 Self::MembershipChange(_) | Self::ProfileChange(_) => {
148 Some(StateEventType::RoomMember.to_string())
149 }
150 Self::OtherState(state) => Some(state.content().event_type().to_string()),
151 Self::FailedToParseMessageLike { event_type, .. } => Some(event_type.to_string()),
152 Self::FailedToParseState { event_type, .. } => Some(event_type.to_string()),
153 Self::CallInvite => Some(MessageLikeEventType::CallInvite.to_string()),
154 Self::RtcNotification { .. } => Some(MessageLikeEventType::RtcNotification.to_string()),
155 }
156 }
157
158 pub async fn from_event(room: &Room, timeline_event: TimelineEvent) -> Option<Self> {
162 let raw_event = timeline_event.into_raw();
163 let deserialized_event = raw_event.deserialize().ok()?;
164
165 match TimelineAction::from_event(
166 deserialized_event,
167 &raw_event,
168 room,
169 None,
170 None,
171 None,
172 None,
173 )
174 .await
175 {
176 Some(TimelineAction::AddItem { content }) => Some(content),
177
178 Some(TimelineAction::HandleAggregation {
180 kind: HandleAggregationKind::BeaconStop { content },
181 ..
182 }) => Some(TimelineItemContent::MsgLike(MsgLikeContent {
183 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(content)),
184 reactions: Default::default(),
185 thread_root: None,
186 in_reply_to: None,
187 thread_summary: None,
188 })),
189
190 Some(TimelineAction::HandleAggregation {
191 kind: HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
192 ..
193 }) => {
194 match TimelineAction::from_content(
196 AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
197 new_content.msgtype,
198 )),
199 None,
200 None,
201 None,
202 ) {
203 TimelineAction::AddItem { content } => Some(content),
204 _ => None,
205 }
206 }
207
208 _ => None,
209 }
210 }
211
212 pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
213 as_variant!(self, TimelineItemContent::MsgLike)
214 }
215
216 pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
220 as_variant!(self, Self::MsgLike(MsgLikeContent {
221 kind: MsgLikeKind::LiveLocation(state),
222 ..
223 }) => state)
224 }
225
226 pub fn as_message(&self) -> Option<&Message> {
229 as_variant!(self, Self::MsgLike(MsgLikeContent {
230 kind: MsgLikeKind::Message(message),
231 ..
232 }) => message)
233 }
234
235 pub fn is_message(&self) -> bool {
238 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
239 }
240
241 pub fn as_poll(&self) -> Option<&PollState> {
244 as_variant!(self, Self::MsgLike(MsgLikeContent {
245 kind: MsgLikeKind::Poll(poll_state),
246 ..
247 }) => poll_state)
248 }
249
250 pub fn is_poll(&self) -> bool {
253 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
254 }
255
256 pub fn as_sticker(&self) -> Option<&Sticker> {
257 as_variant!(
258 self,
259 Self::MsgLike(MsgLikeContent {
260 kind: MsgLikeKind::Sticker(sticker),
261 ..
262 }) => sticker
263 )
264 }
265
266 pub fn is_sticker(&self) -> bool {
269 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
270 }
271
272 pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
275 as_variant!(
276 self,
277 Self::MsgLike(MsgLikeContent {
278 kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
279 ..
280 }) => encrypted_message
281 )
282 }
283
284 pub fn is_unable_to_decrypt(&self) -> bool {
287 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
288 }
289
290 pub fn is_redacted(&self) -> bool {
291 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
292 }
293
294 pub(crate) fn message(
297 msgtype: MessageType,
298 mentions: Option<Mentions>,
299 reactions: ReactionsByKeyBySender,
300 thread_root: Option<OwnedEventId>,
301 in_reply_to: Option<InReplyToDetails>,
302 thread_summary: Option<ThreadSummary>,
303 ) -> Self {
304 let remove_reply_fallback =
305 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
306
307 Self::MsgLike(MsgLikeContent {
308 kind: MsgLikeKind::Message(Message::from_event(
309 msgtype,
310 mentions,
311 None,
312 remove_reply_fallback,
313 )),
314 reactions,
315 thread_root,
316 in_reply_to,
317 thread_summary,
318 })
319 }
320
321 #[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
323 match self {
324 TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
325 TimelineItemContent::MembershipChange(_) => "a membership change",
326 TimelineItemContent::ProfileChange(_) => "a profile change",
327 TimelineItemContent::OtherState(_) => "a state event",
328 TimelineItemContent::FailedToParseMessageLike { .. }
329 | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
330 TimelineItemContent::CallInvite => "a call invite",
331 TimelineItemContent::RtcNotification { .. } => "a call notification",
332 }
333 }
334
335 pub(crate) fn room_member(
336 user_id: OwnedUserId,
337 full_content: StateEventContentChange<RoomMemberEventContent>,
338 sender: OwnedUserId,
339 ) -> Self {
340 use ruma::events::room::member::MembershipChange as MChange;
341 match &full_content {
342 StateEventContentChange::Original { content, prev_content } => {
343 let membership_change = content.membership_change(
344 prev_content.as_ref().map(|c| c.details()),
345 &sender,
346 &user_id,
347 );
348
349 if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
350 membership_change
351 {
352 Self::ProfileChange(MemberProfileChange {
353 user_id,
354 displayname_change: displayname_change.map(|c| Change {
355 new: c.new.map(ToOwned::to_owned),
356 old: c.old.map(ToOwned::to_owned),
357 }),
358 avatar_url_change: avatar_url_change.map(|c| Change {
359 new: c.new.map(ToOwned::to_owned),
360 old: c.old.map(ToOwned::to_owned),
361 }),
362 })
363 } else {
364 let change = match membership_change {
365 MChange::None => MembershipChange::None,
366 MChange::Error => MembershipChange::Error,
367 MChange::Joined => MembershipChange::Joined,
368 MChange::Left => MembershipChange::Left,
369 MChange::Banned => MembershipChange::Banned,
370 MChange::Unbanned => MembershipChange::Unbanned,
371 MChange::Kicked => MembershipChange::Kicked,
372 MChange::Invited => MembershipChange::Invited,
373 MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
374 MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
375 MChange::InvitationRejected => MembershipChange::InvitationRejected,
376 MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
377 MChange::Knocked => MembershipChange::Knocked,
378 MChange::KnockAccepted => MembershipChange::KnockAccepted,
379 MChange::KnockRetracted => MembershipChange::KnockRetracted,
380 MChange::KnockDenied => MembershipChange::KnockDenied,
381 MChange::ProfileChanged { .. } => unreachable!(),
382 _ => MembershipChange::NotImplemented,
383 };
384
385 Self::MembershipChange(RoomMembershipChange {
386 user_id,
387 content: full_content,
388 change: Some(change),
389 })
390 }
391 }
392 StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
393 user_id,
394 content: full_content,
395 change: None,
396 }),
397 }
398 }
399
400 pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
401 match self {
402 Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification { .. } => {
403 TimelineItemContent::MsgLike(MsgLikeContent::redacted())
404 }
405 Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
406 Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
407 Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
408 Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
409 }
410 }
411
412 pub fn thread_root(&self) -> Option<OwnedEventId> {
414 as_variant!(self, Self::MsgLike)?.thread_root.clone()
415 }
416
417 pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
419 as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
420 }
421
422 pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
425 match self {
426 TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
427
428 TimelineItemContent::MembershipChange(..)
429 | TimelineItemContent::ProfileChange(..)
430 | TimelineItemContent::OtherState(..)
431 | TimelineItemContent::FailedToParseMessageLike { .. }
432 | TimelineItemContent::FailedToParseState { .. }
433 | TimelineItemContent::CallInvite
434 | TimelineItemContent::RtcNotification { .. } => {
435 None
437 }
438 }
439 }
440
441 pub fn thread_summary(&self) -> Option<ThreadSummary> {
443 as_variant!(self, Self::MsgLike)?.thread_summary.clone()
444 }
445
446 pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
450 match self {
451 TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
452
453 TimelineItemContent::MembershipChange(..)
454 | TimelineItemContent::ProfileChange(..)
455 | TimelineItemContent::OtherState(..)
456 | TimelineItemContent::FailedToParseMessageLike { .. }
457 | TimelineItemContent::FailedToParseState { .. }
458 | TimelineItemContent::CallInvite
459 | TimelineItemContent::RtcNotification { .. } => {
460 None
462 }
463 }
464 }
465
466 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
467 let mut cloned = self.clone();
468 if let Some(r) = cloned.reactions_mut() {
469 *r = reactions;
470 }
471 cloned
472 }
473}
474
475#[derive(Clone, Debug)]
477pub enum EncryptedMessage {
478 OlmV1Curve25519AesSha2 {
481 sender_key: String,
483 },
484 MegolmV1AesSha2 {
486 #[deprecated = "this field should still be sent but should not be used when received"]
488 #[doc(hidden)] sender_key: Option<String>,
490
491 #[deprecated = "this field should still be sent but should not be used when received"]
493 #[doc(hidden)] device_id: Option<OwnedDeviceId>,
495
496 session_id: String,
498
499 cause: UtdCause,
502 },
503 Unknown,
505}
506
507impl EncryptedMessage {
508 pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
509 match content.scheme {
510 EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
511 Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
512 }
513 #[allow(deprecated)]
514 EncryptedEventScheme::MegolmV1AesSha2(s) => {
515 let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
516
517 Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
518 }
519 _ => Self::Unknown,
520 }
521 }
522
523 pub(crate) fn session_id(&self) -> Option<&str> {
526 match self {
527 EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
528 EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
529 EncryptedMessage::Unknown => None,
530 }
531 }
532}
533
534#[derive(Clone, Debug)]
536pub struct Sticker {
537 pub(in crate::timeline) content: StickerEventContent,
538}
539
540impl Sticker {
541 pub fn content(&self) -> &StickerEventContent {
543 &self.content
544 }
545}
546
547#[derive(Clone, Debug)]
549pub struct RoomMembershipChange {
550 pub(in crate::timeline) user_id: OwnedUserId,
551 pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
552 pub(in crate::timeline) change: Option<MembershipChange>,
553}
554
555impl RoomMembershipChange {
556 pub fn user_id(&self) -> &UserId {
558 &self.user_id
559 }
560
561 pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
563 &self.content
564 }
565
566 pub fn display_name(&self) -> Option<String> {
569 if let StateEventContentChange::Original { content, prev_content } = &self.content {
570 content
571 .displayname
572 .as_ref()
573 .or_else(|| {
574 prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
575 })
576 .cloned()
577 } else {
578 None
579 }
580 }
581
582 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
585 if let StateEventContentChange::Original { content, prev_content } = &self.content {
586 content
587 .avatar_url
588 .as_ref()
589 .or_else(|| {
590 prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
591 })
592 .cloned()
593 } else {
594 None
595 }
596 }
597
598 pub fn change(&self) -> Option<MembershipChange> {
606 self.change
607 }
608
609 fn redact(&self, rules: &RedactionRules) -> Self {
610 Self {
611 user_id: self.user_id.clone(),
612 content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
613 change: self.change,
614 }
615 }
616}
617
618#[derive(Clone, Copy, Debug, PartialEq, Eq)]
620pub enum MembershipChange {
621 None,
623
624 Error,
626
627 Joined,
629
630 Left,
632
633 Banned,
635
636 Unbanned,
638
639 Kicked,
641
642 Invited,
644
645 KickedAndBanned,
647
648 InvitationAccepted,
650
651 InvitationRejected,
653
654 InvitationRevoked,
656
657 Knocked,
659
660 KnockAccepted,
662
663 KnockRetracted,
665
666 KnockDenied,
668
669 NotImplemented,
671}
672
673#[derive(Clone, Debug)]
678pub struct MemberProfileChange {
679 pub(in crate::timeline) user_id: OwnedUserId,
680 pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
681 pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
682}
683
684impl MemberProfileChange {
685 pub fn user_id(&self) -> &UserId {
687 &self.user_id
688 }
689
690 pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
692 self.displayname_change.as_ref()
693 }
694
695 pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
697 self.avatar_url_change.as_ref()
698 }
699
700 fn redact(&self) -> Self {
701 Self {
702 user_id: self.user_id.clone(),
703 displayname_change: None,
708 avatar_url_change: None,
709 }
710 }
711}
712
713#[derive(Clone, Debug)]
716pub enum AnyOtherStateEventContentChange {
717 PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
719
720 PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
722
723 PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
725
726 RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
728
729 RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
731
732 RoomCreate(StateEventContentChange<RoomCreateEventContent>),
734
735 RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
737
738 RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
740
741 RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
743
744 RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
746
747 RoomName(StateEventContentChange<RoomNameEventContent>),
749
750 RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
752
753 RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
755
756 RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
758
759 RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
761
762 RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
764
765 RoomTopic(StateEventContentChange<RoomTopicEventContent>),
767
768 SpaceChild(StateEventContentChange<SpaceChildEventContent>),
770
771 SpaceParent(StateEventContentChange<SpaceParentEventContent>),
773
774 #[doc(hidden)]
775 _Custom { event_type: String },
776}
777
778impl AnyOtherStateEventContentChange {
779 pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
785 let event_type = content.event_type();
786
787 match content {
788 AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
789 AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
790 AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
791 AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
792 AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
793 AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
794 AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
795 AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
796 AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
797 AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
798 AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
799 AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
800 AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
801 AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
802 AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
803 AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
804 AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
805 AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
806 AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
807 AnyStateEventContentChange::RoomMember(_) => unreachable!(),
808 _ => Self::_Custom { event_type: event_type.to_string() },
809 }
810 }
811
812 pub fn event_type(&self) -> StateEventType {
814 match self {
815 Self::PolicyRuleRoom(c) => c.event_type(),
816 Self::PolicyRuleServer(c) => c.event_type(),
817 Self::PolicyRuleUser(c) => c.event_type(),
818 Self::RoomAvatar(c) => c.event_type(),
819 Self::RoomCanonicalAlias(c) => c.event_type(),
820 Self::RoomCreate(c) => c.event_type(),
821 Self::RoomEncryption(c) => c.event_type(),
822 Self::RoomGuestAccess(c) => c.event_type(),
823 Self::RoomHistoryVisibility(c) => c.event_type(),
824 Self::RoomJoinRules(c) => c.event_type(),
825 Self::RoomName(c) => c.event_type(),
826 Self::RoomPinnedEvents(c) => c.event_type(),
827 Self::RoomPowerLevels(c) => c.event_type(),
828 Self::RoomServerAcl(c) => c.event_type(),
829 Self::RoomThirdPartyInvite(c) => c.event_type(),
830 Self::RoomTombstone(c) => c.event_type(),
831 Self::RoomTopic(c) => c.event_type(),
832 Self::SpaceChild(c) => c.event_type(),
833 Self::SpaceParent(c) => c.event_type(),
834 Self::_Custom { event_type } => event_type.as_str().into(),
835 }
836 }
837
838 fn redact(&self, rules: &RedactionRules) -> Self {
839 match self {
840 Self::PolicyRuleRoom(c) => {
841 Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
842 }
843 Self::PolicyRuleServer(c) => {
844 Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
845 }
846 Self::PolicyRuleUser(c) => {
847 Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
848 }
849 Self::RoomAvatar(c) => {
850 Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
851 }
852 Self::RoomCanonicalAlias(c) => {
853 Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
854 }
855 Self::RoomCreate(c) => {
856 Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
857 }
858 Self::RoomEncryption(c) => {
859 Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
860 }
861 Self::RoomGuestAccess(c) => {
862 Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
863 }
864 Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
865 StateEventContentChange::Redacted(c.clone().redact(rules)),
866 ),
867 Self::RoomJoinRules(c) => {
868 Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
869 }
870 Self::RoomName(c) => {
871 Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
872 }
873 Self::RoomPinnedEvents(c) => {
874 Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
875 }
876 Self::RoomPowerLevels(c) => {
877 Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
878 }
879 Self::RoomServerAcl(c) => {
880 Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
881 }
882 Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
883 StateEventContentChange::Redacted(c.clone().redact(rules)),
884 ),
885 Self::RoomTombstone(c) => {
886 Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
887 }
888 Self::RoomTopic(c) => {
889 Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
890 }
891 Self::SpaceChild(c) => {
892 Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
893 }
894 Self::SpaceParent(c) => {
895 Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
896 }
897 Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
898 }
899 }
900}
901
902#[derive(Clone, Debug)]
904pub struct OtherState {
905 pub(in crate::timeline) state_key: String,
906 pub(in crate::timeline) content: AnyOtherStateEventContentChange,
907}
908
909impl OtherState {
910 pub fn state_key(&self) -> &str {
912 &self.state_key
913 }
914
915 pub fn content(&self) -> &AnyOtherStateEventContentChange {
917 &self.content
918 }
919
920 fn redact(&self, rules: &RedactionRules) -> Self {
921 Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
922 }
923}
924
925#[cfg(test)]
926mod tests {
927 use assert_matches2::assert_let;
928 use matrix_sdk_test::ALICE;
929 use ruma::{
930 assign,
931 events::{
932 StateEventContentChange,
933 room::member::{
934 MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
935 },
936 },
937 room_version_rules::RedactionRules,
938 };
939
940 use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
941
942 #[test]
943 fn redact_membership_change() {
944 let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
945 user_id: ALICE.to_owned(),
946 content: StateEventContentChange::Original {
947 content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
948 reason: Some("🤬".to_owned()),
949 }),
950 prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
951 MembershipState::Join,
952 )),
953 },
954 change: Some(MembershipChange::Banned),
955 });
956
957 let redacted = content.redact(&RedactionRules::V11);
958 assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
959 assert_eq!(inner.change, Some(MembershipChange::Banned));
960 assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
961 assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
962 }
963}