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 [
185 TimelineAction::AddItem { content },
186 TimelineAction::HandleAggregation {
187 kind: HandleAggregationKind::BeaconStop { .. },
188 ..
189 },
190 ] => {
191 if content.is_live_location_state() {
192 Some(content.clone())
193 } else {
194 warn!(
195 "Unexpected [AddItem, BeaconStop] actions with a non-live-location \
196 AddItem; ignoring event"
197 );
198 None
199 }
200 }
201 [
203 TimelineAction::HandleAggregation {
204 kind: HandleAggregationKind::BeaconStop { content, .. },
205 ..
206 },
207 ] => Some(TimelineItemContent::MsgLike(MsgLikeContent {
208 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(content.clone())),
209 reactions: Default::default(),
210 thread_root: None,
211 in_reply_to: None,
212 thread_summary: None,
213 })),
214 [
215 TimelineAction::HandleAggregation {
216 kind:
217 HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
218 ..
219 },
220 ] => {
221 match TimelineAction::from_content(
223 AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
224 new_content.msgtype.clone(),
225 )),
226 None,
227 None,
228 None,
229 ) {
230 TimelineAction::AddItem { content } => Some(content),
231 _ => None,
232 }
233 }
234 [] => {
235 warn!("No action for event content processing");
236 None
237 }
238 [_, _, ..] => {
239 warn!("Ignoring event that produced multiple timeline actions");
241 None
242 }
243 _ => None,
244 }
245 }
246
247 pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
248 as_variant!(self, TimelineItemContent::MsgLike)
249 }
250
251 pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
255 as_variant!(self, Self::MsgLike(MsgLikeContent {
256 kind: MsgLikeKind::LiveLocation(state),
257 ..
258 }) => state)
259 }
260
261 pub fn is_live_location_state(&self) -> bool {
264 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::LiveLocation(_), .. }))
265 }
266
267 pub fn as_message(&self) -> Option<&Message> {
270 as_variant!(self, Self::MsgLike(MsgLikeContent {
271 kind: MsgLikeKind::Message(message),
272 ..
273 }) => message)
274 }
275
276 pub fn is_message(&self) -> bool {
279 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
280 }
281
282 pub fn as_poll(&self) -> Option<&PollState> {
285 as_variant!(self, Self::MsgLike(MsgLikeContent {
286 kind: MsgLikeKind::Poll(poll_state),
287 ..
288 }) => poll_state)
289 }
290
291 pub fn is_poll(&self) -> bool {
294 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
295 }
296
297 pub fn as_sticker(&self) -> Option<&Sticker> {
298 as_variant!(
299 self,
300 Self::MsgLike(MsgLikeContent {
301 kind: MsgLikeKind::Sticker(sticker),
302 ..
303 }) => sticker
304 )
305 }
306
307 pub fn is_sticker(&self) -> bool {
310 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
311 }
312
313 pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
316 as_variant!(
317 self,
318 Self::MsgLike(MsgLikeContent {
319 kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
320 ..
321 }) => encrypted_message
322 )
323 }
324
325 pub fn is_unable_to_decrypt(&self) -> bool {
328 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
329 }
330
331 pub fn is_redacted(&self) -> bool {
332 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
333 }
334
335 pub(crate) fn message(
338 msgtype: MessageType,
339 mentions: Option<Mentions>,
340 reactions: ReactionsByKeyBySender,
341 thread_root: Option<OwnedEventId>,
342 in_reply_to: Option<InReplyToDetails>,
343 thread_summary: Option<ThreadSummary>,
344 ) -> Self {
345 let remove_reply_fallback =
346 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
347
348 Self::MsgLike(MsgLikeContent {
349 kind: MsgLikeKind::Message(Message::from_event(
350 msgtype,
351 mentions,
352 None,
353 remove_reply_fallback,
354 )),
355 reactions,
356 thread_root,
357 in_reply_to,
358 thread_summary,
359 })
360 }
361
362 #[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
364 match self {
365 TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
366 TimelineItemContent::MembershipChange(_) => "a membership change",
367 TimelineItemContent::ProfileChange(_) => "a profile change",
368 TimelineItemContent::OtherState(_) => "a state event",
369 TimelineItemContent::FailedToParseMessageLike { .. }
370 | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
371 TimelineItemContent::CallInvite => "a call invite",
372 TimelineItemContent::RtcNotification { .. } => "a call notification",
373 }
374 }
375
376 pub(crate) fn room_member(
377 user_id: OwnedUserId,
378 full_content: StateEventContentChange<RoomMemberEventContent>,
379 sender: OwnedUserId,
380 ) -> Self {
381 use ruma::events::room::member::MembershipChange as MChange;
382 match &full_content {
383 StateEventContentChange::Original { content, prev_content } => {
384 let membership_change = content.membership_change(
385 prev_content.as_ref().map(|c| c.details()),
386 &sender,
387 &user_id,
388 );
389
390 if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
391 membership_change
392 {
393 Self::ProfileChange(MemberProfileChange {
394 user_id,
395 displayname_change: displayname_change.map(|c| Change {
396 new: c.new.map(ToOwned::to_owned),
397 old: c.old.map(ToOwned::to_owned),
398 }),
399 avatar_url_change: avatar_url_change.map(|c| Change {
400 new: c.new.map(ToOwned::to_owned),
401 old: c.old.map(ToOwned::to_owned),
402 }),
403 })
404 } else {
405 let change = match membership_change {
406 MChange::None => MembershipChange::None,
407 MChange::Error => MembershipChange::Error,
408 MChange::Joined => MembershipChange::Joined,
409 MChange::Left => MembershipChange::Left,
410 MChange::Banned => MembershipChange::Banned,
411 MChange::Unbanned => MembershipChange::Unbanned,
412 MChange::Kicked => MembershipChange::Kicked,
413 MChange::Invited => MembershipChange::Invited,
414 MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
415 MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
416 MChange::InvitationRejected => MembershipChange::InvitationRejected,
417 MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
418 MChange::Knocked => MembershipChange::Knocked,
419 MChange::KnockAccepted => MembershipChange::KnockAccepted,
420 MChange::KnockRetracted => MembershipChange::KnockRetracted,
421 MChange::KnockDenied => MembershipChange::KnockDenied,
422 MChange::ProfileChanged { .. } => unreachable!(),
423 _ => MembershipChange::NotImplemented,
424 };
425
426 Self::MembershipChange(RoomMembershipChange {
427 user_id,
428 content: full_content,
429 change: Some(change),
430 })
431 }
432 }
433 StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
434 user_id,
435 content: full_content,
436 change: None,
437 }),
438 }
439 }
440
441 pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
442 match self {
443 Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification { .. } => {
444 TimelineItemContent::MsgLike(MsgLikeContent::redacted())
445 }
446 Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
447 Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
448 Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
449 Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
450 }
451 }
452
453 pub fn thread_root(&self) -> Option<OwnedEventId> {
455 as_variant!(self, Self::MsgLike)?.thread_root.clone()
456 }
457
458 pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
460 as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
461 }
462
463 pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
466 match self {
467 TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
468
469 TimelineItemContent::MembershipChange(..)
470 | TimelineItemContent::ProfileChange(..)
471 | TimelineItemContent::OtherState(..)
472 | TimelineItemContent::FailedToParseMessageLike { .. }
473 | TimelineItemContent::FailedToParseState { .. }
474 | TimelineItemContent::CallInvite
475 | TimelineItemContent::RtcNotification { .. } => {
476 None
478 }
479 }
480 }
481
482 pub fn thread_summary(&self) -> Option<ThreadSummary> {
484 as_variant!(self, Self::MsgLike)?.thread_summary.clone()
485 }
486
487 pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
491 match self {
492 TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
493
494 TimelineItemContent::MembershipChange(..)
495 | TimelineItemContent::ProfileChange(..)
496 | TimelineItemContent::OtherState(..)
497 | TimelineItemContent::FailedToParseMessageLike { .. }
498 | TimelineItemContent::FailedToParseState { .. }
499 | TimelineItemContent::CallInvite
500 | TimelineItemContent::RtcNotification { .. } => {
501 None
503 }
504 }
505 }
506
507 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
508 let mut cloned = self.clone();
509 if let Some(r) = cloned.reactions_mut() {
510 *r = reactions;
511 }
512 cloned
513 }
514}
515
516#[derive(Clone, Debug)]
518pub enum EncryptedMessage {
519 OlmV1Curve25519AesSha2 {
522 sender_key: String,
524 },
525 MegolmV1AesSha2 {
527 #[deprecated = "this field should still be sent but should not be used when received"]
529 #[doc(hidden)] sender_key: Option<String>,
531
532 #[deprecated = "this field should still be sent but should not be used when received"]
534 #[doc(hidden)] device_id: Option<OwnedDeviceId>,
536
537 session_id: String,
539
540 cause: UtdCause,
543 },
544 Unknown,
546}
547
548impl EncryptedMessage {
549 pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
550 match content.scheme {
551 EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
552 Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
553 }
554 #[allow(deprecated)]
555 EncryptedEventScheme::MegolmV1AesSha2(s) => {
556 let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
557
558 Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
559 }
560 _ => Self::Unknown,
561 }
562 }
563
564 pub(crate) fn session_id(&self) -> Option<&str> {
567 match self {
568 EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
569 EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
570 EncryptedMessage::Unknown => None,
571 }
572 }
573}
574
575#[derive(Clone, Debug)]
577pub struct Sticker {
578 pub(in crate::timeline) content: StickerEventContent,
579}
580
581impl Sticker {
582 pub fn content(&self) -> &StickerEventContent {
584 &self.content
585 }
586}
587
588#[derive(Clone, Debug)]
590pub struct RoomMembershipChange {
591 pub(in crate::timeline) user_id: OwnedUserId,
592 pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
593 pub(in crate::timeline) change: Option<MembershipChange>,
594}
595
596impl RoomMembershipChange {
597 pub fn user_id(&self) -> &UserId {
599 &self.user_id
600 }
601
602 pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
604 &self.content
605 }
606
607 pub fn display_name(&self) -> Option<String> {
610 if let StateEventContentChange::Original { content, prev_content } = &self.content {
611 content
612 .displayname
613 .as_ref()
614 .or_else(|| {
615 prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
616 })
617 .cloned()
618 } else {
619 None
620 }
621 }
622
623 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
626 if let StateEventContentChange::Original { content, prev_content } = &self.content {
627 content
628 .avatar_url
629 .as_ref()
630 .or_else(|| {
631 prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
632 })
633 .cloned()
634 } else {
635 None
636 }
637 }
638
639 pub fn change(&self) -> Option<MembershipChange> {
647 self.change
648 }
649
650 fn redact(&self, rules: &RedactionRules) -> Self {
651 Self {
652 user_id: self.user_id.clone(),
653 content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
654 change: self.change,
655 }
656 }
657}
658
659#[derive(Clone, Copy, Debug, PartialEq, Eq)]
661pub enum MembershipChange {
662 None,
664
665 Error,
667
668 Joined,
670
671 Left,
673
674 Banned,
676
677 Unbanned,
679
680 Kicked,
682
683 Invited,
685
686 KickedAndBanned,
688
689 InvitationAccepted,
691
692 InvitationRejected,
694
695 InvitationRevoked,
697
698 Knocked,
700
701 KnockAccepted,
703
704 KnockRetracted,
706
707 KnockDenied,
709
710 NotImplemented,
712}
713
714#[derive(Clone, Debug)]
719pub struct MemberProfileChange {
720 pub(in crate::timeline) user_id: OwnedUserId,
721 pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
722 pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
723}
724
725impl MemberProfileChange {
726 pub fn user_id(&self) -> &UserId {
728 &self.user_id
729 }
730
731 pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
733 self.displayname_change.as_ref()
734 }
735
736 pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
738 self.avatar_url_change.as_ref()
739 }
740
741 fn redact(&self) -> Self {
742 Self {
743 user_id: self.user_id.clone(),
744 displayname_change: None,
749 avatar_url_change: None,
750 }
751 }
752}
753
754#[derive(Clone, Debug)]
757pub enum AnyOtherStateEventContentChange {
758 PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
760
761 PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
763
764 PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
766
767 RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
769
770 RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
772
773 RoomCreate(StateEventContentChange<RoomCreateEventContent>),
775
776 RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
778
779 RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
781
782 RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
784
785 RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
787
788 RoomName(StateEventContentChange<RoomNameEventContent>),
790
791 RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
793
794 RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
796
797 RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
799
800 RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
802
803 RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
805
806 RoomTopic(StateEventContentChange<RoomTopicEventContent>),
808
809 SpaceChild(StateEventContentChange<SpaceChildEventContent>),
811
812 SpaceParent(StateEventContentChange<SpaceParentEventContent>),
814
815 #[doc(hidden)]
816 _Custom { event_type: String },
817}
818
819impl AnyOtherStateEventContentChange {
820 pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
826 let event_type = content.event_type();
827
828 match content {
829 AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
830 AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
831 AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
832 AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
833 AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
834 AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
835 AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
836 AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
837 AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
838 AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
839 AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
840 AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
841 AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
842 AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
843 AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
844 AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
845 AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
846 AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
847 AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
848 AnyStateEventContentChange::RoomMember(_) => unreachable!(),
849 _ => Self::_Custom { event_type: event_type.to_string() },
850 }
851 }
852
853 pub fn event_type(&self) -> StateEventType {
855 match self {
856 Self::PolicyRuleRoom(c) => c.event_type(),
857 Self::PolicyRuleServer(c) => c.event_type(),
858 Self::PolicyRuleUser(c) => c.event_type(),
859 Self::RoomAvatar(c) => c.event_type(),
860 Self::RoomCanonicalAlias(c) => c.event_type(),
861 Self::RoomCreate(c) => c.event_type(),
862 Self::RoomEncryption(c) => c.event_type(),
863 Self::RoomGuestAccess(c) => c.event_type(),
864 Self::RoomHistoryVisibility(c) => c.event_type(),
865 Self::RoomJoinRules(c) => c.event_type(),
866 Self::RoomName(c) => c.event_type(),
867 Self::RoomPinnedEvents(c) => c.event_type(),
868 Self::RoomPowerLevels(c) => c.event_type(),
869 Self::RoomServerAcl(c) => c.event_type(),
870 Self::RoomThirdPartyInvite(c) => c.event_type(),
871 Self::RoomTombstone(c) => c.event_type(),
872 Self::RoomTopic(c) => c.event_type(),
873 Self::SpaceChild(c) => c.event_type(),
874 Self::SpaceParent(c) => c.event_type(),
875 Self::_Custom { event_type } => event_type.as_str().into(),
876 }
877 }
878
879 fn redact(&self, rules: &RedactionRules) -> Self {
880 match self {
881 Self::PolicyRuleRoom(c) => {
882 Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
883 }
884 Self::PolicyRuleServer(c) => {
885 Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
886 }
887 Self::PolicyRuleUser(c) => {
888 Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
889 }
890 Self::RoomAvatar(c) => {
891 Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
892 }
893 Self::RoomCanonicalAlias(c) => {
894 Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
895 }
896 Self::RoomCreate(c) => {
897 Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
898 }
899 Self::RoomEncryption(c) => {
900 Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
901 }
902 Self::RoomGuestAccess(c) => {
903 Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
904 }
905 Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
906 StateEventContentChange::Redacted(c.clone().redact(rules)),
907 ),
908 Self::RoomJoinRules(c) => {
909 Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
910 }
911 Self::RoomName(c) => {
912 Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
913 }
914 Self::RoomPinnedEvents(c) => {
915 Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
916 }
917 Self::RoomPowerLevels(c) => {
918 Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
919 }
920 Self::RoomServerAcl(c) => {
921 Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
922 }
923 Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
924 StateEventContentChange::Redacted(c.clone().redact(rules)),
925 ),
926 Self::RoomTombstone(c) => {
927 Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
928 }
929 Self::RoomTopic(c) => {
930 Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
931 }
932 Self::SpaceChild(c) => {
933 Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
934 }
935 Self::SpaceParent(c) => {
936 Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
937 }
938 Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
939 }
940 }
941}
942
943#[derive(Clone, Debug)]
945pub struct OtherState {
946 pub(in crate::timeline) state_key: String,
947 pub(in crate::timeline) content: AnyOtherStateEventContentChange,
948}
949
950impl OtherState {
951 pub fn state_key(&self) -> &str {
953 &self.state_key
954 }
955
956 pub fn content(&self) -> &AnyOtherStateEventContentChange {
958 &self.content
959 }
960
961 fn redact(&self, rules: &RedactionRules) -> Self {
962 Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
963 }
964}
965
966#[cfg(test)]
967mod tests {
968 use assert_matches2::assert_let;
969 use matrix_sdk_test::ALICE;
970 use ruma::{
971 assign,
972 events::{
973 StateEventContentChange,
974 room::member::{
975 MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
976 },
977 },
978 room_version_rules::RedactionRules,
979 };
980
981 use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
982
983 #[test]
984 fn redact_membership_change() {
985 let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
986 user_id: ALICE.to_owned(),
987 content: StateEventContentChange::Original {
988 content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
989 reason: Some("🤬".to_owned()),
990 }),
991 prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
992 MembershipState::Join,
993 )),
994 },
995 change: Some(MembershipChange::Banned),
996 });
997
998 let redacted = content.redact(&RedactionRules::V11);
999 assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
1000 assert_eq!(inner.change, Some(MembershipChange::Banned));
1001 assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
1002 assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
1003 }
1004}