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 },
128}
129
130impl TimelineItemContent {
131 pub fn event_type_str(&self) -> Option<String> {
135 match self {
136 Self::MsgLike(msg) => Some(match &msg.kind {
137 MsgLikeKind::Message(_) => MessageLikeEventType::RoomMessage.to_string(),
138 MsgLikeKind::Sticker(_) => MessageLikeEventType::Sticker.to_string(),
139 MsgLikeKind::Poll(_) => MessageLikeEventType::PollStart.to_string(),
140 MsgLikeKind::Redacted => return None,
141 MsgLikeKind::UnableToDecrypt(_) => MessageLikeEventType::RoomEncrypted.to_string(),
142 MsgLikeKind::Other(other) => other.event_type().to_string(),
143 MsgLikeKind::LiveLocation(_) => StateEventType::BeaconInfo.to_string(),
144 }),
145 Self::MembershipChange(_) | Self::ProfileChange(_) => {
146 Some(StateEventType::RoomMember.to_string())
147 }
148 Self::OtherState(state) => Some(state.content().event_type().to_string()),
149 Self::FailedToParseMessageLike { event_type, .. } => Some(event_type.to_string()),
150 Self::FailedToParseState { event_type, .. } => Some(event_type.to_string()),
151 Self::CallInvite => Some(MessageLikeEventType::CallInvite.to_string()),
152 Self::RtcNotification { .. } => Some(MessageLikeEventType::RtcNotification.to_string()),
153 }
154 }
155
156 pub async fn from_event(room: &Room, timeline_event: TimelineEvent) -> Option<Self> {
160 let raw_event = timeline_event.into_raw();
161 let deserialized_event = raw_event.deserialize().ok()?;
162
163 match TimelineAction::from_event(
164 deserialized_event,
165 &raw_event,
166 room,
167 None,
168 None,
169 None,
170 None,
171 )
172 .await
173 {
174 Some(TimelineAction::AddItem { content }) => Some(content),
175
176 Some(TimelineAction::HandleAggregation {
178 kind: HandleAggregationKind::BeaconStop { content },
179 ..
180 }) => Some(TimelineItemContent::MsgLike(MsgLikeContent {
181 kind: MsgLikeKind::LiveLocation(LiveLocationState::new(content)),
182 reactions: Default::default(),
183 thread_root: None,
184 in_reply_to: None,
185 thread_summary: None,
186 })),
187
188 Some(TimelineAction::HandleAggregation {
189 kind: HandleAggregationKind::Edit { replacement: Replacement { new_content, .. } },
190 ..
191 }) => {
192 match TimelineAction::from_content(
194 AnyMessageLikeEventContent::RoomMessage(RoomMessageEventContent::new(
195 new_content.msgtype,
196 )),
197 None,
198 None,
199 None,
200 ) {
201 TimelineAction::AddItem { content } => Some(content),
202 _ => None,
203 }
204 }
205
206 _ => None,
207 }
208 }
209
210 pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
211 as_variant!(self, TimelineItemContent::MsgLike)
212 }
213
214 pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
218 as_variant!(self, Self::MsgLike(MsgLikeContent {
219 kind: MsgLikeKind::LiveLocation(state),
220 ..
221 }) => state)
222 }
223
224 pub(in crate::timeline) fn as_live_location_state_mut(
228 &mut self,
229 ) -> Option<&mut LiveLocationState> {
230 as_variant!(self, Self::MsgLike(MsgLikeContent {
231 kind: MsgLikeKind::LiveLocation(state),
232 ..
233 }) => state)
234 }
235
236 pub fn is_live_location(&self) -> bool {
239 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::LiveLocation(_), .. }))
240 }
241
242 pub fn as_message(&self) -> Option<&Message> {
245 as_variant!(self, Self::MsgLike(MsgLikeContent {
246 kind: MsgLikeKind::Message(message),
247 ..
248 }) => message)
249 }
250
251 pub fn is_message(&self) -> bool {
254 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
255 }
256
257 pub fn as_poll(&self) -> Option<&PollState> {
260 as_variant!(self, Self::MsgLike(MsgLikeContent {
261 kind: MsgLikeKind::Poll(poll_state),
262 ..
263 }) => poll_state)
264 }
265
266 pub fn is_poll(&self) -> bool {
269 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
270 }
271
272 pub fn as_sticker(&self) -> Option<&Sticker> {
273 as_variant!(
274 self,
275 Self::MsgLike(MsgLikeContent {
276 kind: MsgLikeKind::Sticker(sticker),
277 ..
278 }) => sticker
279 )
280 }
281
282 pub fn is_sticker(&self) -> bool {
285 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
286 }
287
288 pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
291 as_variant!(
292 self,
293 Self::MsgLike(MsgLikeContent {
294 kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
295 ..
296 }) => encrypted_message
297 )
298 }
299
300 pub fn is_unable_to_decrypt(&self) -> bool {
303 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
304 }
305
306 pub fn is_redacted(&self) -> bool {
307 matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
308 }
309
310 pub(crate) fn message(
313 msgtype: MessageType,
314 mentions: Option<Mentions>,
315 reactions: ReactionsByKeyBySender,
316 thread_root: Option<OwnedEventId>,
317 in_reply_to: Option<InReplyToDetails>,
318 thread_summary: Option<ThreadSummary>,
319 ) -> Self {
320 let remove_reply_fallback =
321 if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
322
323 Self::MsgLike(MsgLikeContent {
324 kind: MsgLikeKind::Message(Message::from_event(
325 msgtype,
326 mentions,
327 None,
328 remove_reply_fallback,
329 )),
330 reactions,
331 thread_root,
332 in_reply_to,
333 thread_summary,
334 })
335 }
336
337 #[cfg(not(tarpaulin_include))] pub(crate) fn debug_string(&self) -> &'static str {
339 match self {
340 TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
341 TimelineItemContent::MembershipChange(_) => "a membership change",
342 TimelineItemContent::ProfileChange(_) => "a profile change",
343 TimelineItemContent::OtherState(_) => "a state event",
344 TimelineItemContent::FailedToParseMessageLike { .. }
345 | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
346 TimelineItemContent::CallInvite => "a call invite",
347 TimelineItemContent::RtcNotification { .. } => "a call notification",
348 }
349 }
350
351 pub(crate) fn room_member(
352 user_id: OwnedUserId,
353 full_content: StateEventContentChange<RoomMemberEventContent>,
354 sender: OwnedUserId,
355 ) -> Self {
356 use ruma::events::room::member::MembershipChange as MChange;
357 match &full_content {
358 StateEventContentChange::Original { content, prev_content } => {
359 let membership_change = content.membership_change(
360 prev_content.as_ref().map(|c| c.details()),
361 &sender,
362 &user_id,
363 );
364
365 if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
366 membership_change
367 {
368 Self::ProfileChange(MemberProfileChange {
369 user_id,
370 displayname_change: displayname_change.map(|c| Change {
371 new: c.new.map(ToOwned::to_owned),
372 old: c.old.map(ToOwned::to_owned),
373 }),
374 avatar_url_change: avatar_url_change.map(|c| Change {
375 new: c.new.map(ToOwned::to_owned),
376 old: c.old.map(ToOwned::to_owned),
377 }),
378 })
379 } else {
380 let change = match membership_change {
381 MChange::None => MembershipChange::None,
382 MChange::Error => MembershipChange::Error,
383 MChange::Joined => MembershipChange::Joined,
384 MChange::Left => MembershipChange::Left,
385 MChange::Banned => MembershipChange::Banned,
386 MChange::Unbanned => MembershipChange::Unbanned,
387 MChange::Kicked => MembershipChange::Kicked,
388 MChange::Invited => MembershipChange::Invited,
389 MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
390 MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
391 MChange::InvitationRejected => MembershipChange::InvitationRejected,
392 MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
393 MChange::Knocked => MembershipChange::Knocked,
394 MChange::KnockAccepted => MembershipChange::KnockAccepted,
395 MChange::KnockRetracted => MembershipChange::KnockRetracted,
396 MChange::KnockDenied => MembershipChange::KnockDenied,
397 MChange::ProfileChanged { .. } => unreachable!(),
398 _ => MembershipChange::NotImplemented,
399 };
400
401 Self::MembershipChange(RoomMembershipChange {
402 user_id,
403 content: full_content,
404 change: Some(change),
405 })
406 }
407 }
408 StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
409 user_id,
410 content: full_content,
411 change: None,
412 }),
413 }
414 }
415
416 pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
417 match self {
418 Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification { .. } => {
419 TimelineItemContent::MsgLike(MsgLikeContent::redacted())
420 }
421 Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
422 Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
423 Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
424 Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
425 }
426 }
427
428 pub fn thread_root(&self) -> Option<OwnedEventId> {
430 as_variant!(self, Self::MsgLike)?.thread_root.clone()
431 }
432
433 pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
435 as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
436 }
437
438 pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
441 match self {
442 TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
443
444 TimelineItemContent::MembershipChange(..)
445 | TimelineItemContent::ProfileChange(..)
446 | TimelineItemContent::OtherState(..)
447 | TimelineItemContent::FailedToParseMessageLike { .. }
448 | TimelineItemContent::FailedToParseState { .. }
449 | TimelineItemContent::CallInvite
450 | TimelineItemContent::RtcNotification { .. } => {
451 None
453 }
454 }
455 }
456
457 pub fn thread_summary(&self) -> Option<ThreadSummary> {
459 as_variant!(self, Self::MsgLike)?.thread_summary.clone()
460 }
461
462 pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
466 match self {
467 TimelineItemContent::MsgLike(msglike) => Some(&mut 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 with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
483 let mut cloned = self.clone();
484 if let Some(r) = cloned.reactions_mut() {
485 *r = reactions;
486 }
487 cloned
488 }
489}
490
491#[derive(Clone, Debug)]
493pub enum EncryptedMessage {
494 OlmV1Curve25519AesSha2 {
497 sender_key: String,
499 },
500 MegolmV1AesSha2 {
502 #[deprecated = "this field should still be sent but should not be used when received"]
504 #[doc(hidden)] sender_key: Option<String>,
506
507 #[deprecated = "this field should still be sent but should not be used when received"]
509 #[doc(hidden)] device_id: Option<OwnedDeviceId>,
511
512 session_id: String,
514
515 cause: UtdCause,
518 },
519 Unknown,
521}
522
523impl EncryptedMessage {
524 pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
525 match content.scheme {
526 EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
527 Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
528 }
529 #[allow(deprecated)]
530 EncryptedEventScheme::MegolmV1AesSha2(s) => {
531 let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
532
533 Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
534 }
535 _ => Self::Unknown,
536 }
537 }
538
539 pub(crate) fn session_id(&self) -> Option<&str> {
542 match self {
543 EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
544 EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
545 EncryptedMessage::Unknown => None,
546 }
547 }
548}
549
550#[derive(Clone, Debug)]
552pub struct Sticker {
553 pub(in crate::timeline) content: StickerEventContent,
554}
555
556impl Sticker {
557 pub fn content(&self) -> &StickerEventContent {
559 &self.content
560 }
561}
562
563#[derive(Clone, Debug)]
565pub struct RoomMembershipChange {
566 pub(in crate::timeline) user_id: OwnedUserId,
567 pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
568 pub(in crate::timeline) change: Option<MembershipChange>,
569}
570
571impl RoomMembershipChange {
572 pub fn user_id(&self) -> &UserId {
574 &self.user_id
575 }
576
577 pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
579 &self.content
580 }
581
582 pub fn display_name(&self) -> Option<String> {
585 if let StateEventContentChange::Original { content, prev_content } = &self.content {
586 content
587 .displayname
588 .as_ref()
589 .or_else(|| {
590 prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
591 })
592 .cloned()
593 } else {
594 None
595 }
596 }
597
598 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
601 if let StateEventContentChange::Original { content, prev_content } = &self.content {
602 content
603 .avatar_url
604 .as_ref()
605 .or_else(|| {
606 prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
607 })
608 .cloned()
609 } else {
610 None
611 }
612 }
613
614 pub fn change(&self) -> Option<MembershipChange> {
622 self.change
623 }
624
625 fn redact(&self, rules: &RedactionRules) -> Self {
626 Self {
627 user_id: self.user_id.clone(),
628 content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
629 change: self.change,
630 }
631 }
632}
633
634#[derive(Clone, Copy, Debug, PartialEq, Eq)]
636pub enum MembershipChange {
637 None,
639
640 Error,
642
643 Joined,
645
646 Left,
648
649 Banned,
651
652 Unbanned,
654
655 Kicked,
657
658 Invited,
660
661 KickedAndBanned,
663
664 InvitationAccepted,
666
667 InvitationRejected,
669
670 InvitationRevoked,
672
673 Knocked,
675
676 KnockAccepted,
678
679 KnockRetracted,
681
682 KnockDenied,
684
685 NotImplemented,
687}
688
689#[derive(Clone, Debug)]
694pub struct MemberProfileChange {
695 pub(in crate::timeline) user_id: OwnedUserId,
696 pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
697 pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
698}
699
700impl MemberProfileChange {
701 pub fn user_id(&self) -> &UserId {
703 &self.user_id
704 }
705
706 pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
708 self.displayname_change.as_ref()
709 }
710
711 pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
713 self.avatar_url_change.as_ref()
714 }
715
716 fn redact(&self) -> Self {
717 Self {
718 user_id: self.user_id.clone(),
719 displayname_change: None,
724 avatar_url_change: None,
725 }
726 }
727}
728
729#[derive(Clone, Debug)]
732pub enum AnyOtherStateEventContentChange {
733 PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
735
736 PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
738
739 PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
741
742 RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
744
745 RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
747
748 RoomCreate(StateEventContentChange<RoomCreateEventContent>),
750
751 RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
753
754 RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
756
757 RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
759
760 RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
762
763 RoomName(StateEventContentChange<RoomNameEventContent>),
765
766 RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
768
769 RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
771
772 RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
774
775 RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
777
778 RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
780
781 RoomTopic(StateEventContentChange<RoomTopicEventContent>),
783
784 SpaceChild(StateEventContentChange<SpaceChildEventContent>),
786
787 SpaceParent(StateEventContentChange<SpaceParentEventContent>),
789
790 #[doc(hidden)]
791 _Custom { event_type: String },
792}
793
794impl AnyOtherStateEventContentChange {
795 pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
801 let event_type = content.event_type();
802
803 match content {
804 AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
805 AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
806 AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
807 AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
808 AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
809 AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
810 AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
811 AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
812 AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
813 AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
814 AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
815 AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
816 AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
817 AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
818 AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
819 AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
820 AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
821 AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
822 AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
823 AnyStateEventContentChange::RoomMember(_) => unreachable!(),
824 _ => Self::_Custom { event_type: event_type.to_string() },
825 }
826 }
827
828 pub fn event_type(&self) -> StateEventType {
830 match self {
831 Self::PolicyRuleRoom(c) => c.event_type(),
832 Self::PolicyRuleServer(c) => c.event_type(),
833 Self::PolicyRuleUser(c) => c.event_type(),
834 Self::RoomAvatar(c) => c.event_type(),
835 Self::RoomCanonicalAlias(c) => c.event_type(),
836 Self::RoomCreate(c) => c.event_type(),
837 Self::RoomEncryption(c) => c.event_type(),
838 Self::RoomGuestAccess(c) => c.event_type(),
839 Self::RoomHistoryVisibility(c) => c.event_type(),
840 Self::RoomJoinRules(c) => c.event_type(),
841 Self::RoomName(c) => c.event_type(),
842 Self::RoomPinnedEvents(c) => c.event_type(),
843 Self::RoomPowerLevels(c) => c.event_type(),
844 Self::RoomServerAcl(c) => c.event_type(),
845 Self::RoomThirdPartyInvite(c) => c.event_type(),
846 Self::RoomTombstone(c) => c.event_type(),
847 Self::RoomTopic(c) => c.event_type(),
848 Self::SpaceChild(c) => c.event_type(),
849 Self::SpaceParent(c) => c.event_type(),
850 Self::_Custom { event_type } => event_type.as_str().into(),
851 }
852 }
853
854 fn redact(&self, rules: &RedactionRules) -> Self {
855 match self {
856 Self::PolicyRuleRoom(c) => {
857 Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
858 }
859 Self::PolicyRuleServer(c) => {
860 Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
861 }
862 Self::PolicyRuleUser(c) => {
863 Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
864 }
865 Self::RoomAvatar(c) => {
866 Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
867 }
868 Self::RoomCanonicalAlias(c) => {
869 Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
870 }
871 Self::RoomCreate(c) => {
872 Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
873 }
874 Self::RoomEncryption(c) => {
875 Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
876 }
877 Self::RoomGuestAccess(c) => {
878 Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
879 }
880 Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
881 StateEventContentChange::Redacted(c.clone().redact(rules)),
882 ),
883 Self::RoomJoinRules(c) => {
884 Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
885 }
886 Self::RoomName(c) => {
887 Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
888 }
889 Self::RoomPinnedEvents(c) => {
890 Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
891 }
892 Self::RoomPowerLevels(c) => {
893 Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
894 }
895 Self::RoomServerAcl(c) => {
896 Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
897 }
898 Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
899 StateEventContentChange::Redacted(c.clone().redact(rules)),
900 ),
901 Self::RoomTombstone(c) => {
902 Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
903 }
904 Self::RoomTopic(c) => {
905 Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
906 }
907 Self::SpaceChild(c) => {
908 Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
909 }
910 Self::SpaceParent(c) => {
911 Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
912 }
913 Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
914 }
915 }
916}
917
918#[derive(Clone, Debug)]
920pub struct OtherState {
921 pub(in crate::timeline) state_key: String,
922 pub(in crate::timeline) content: AnyOtherStateEventContentChange,
923}
924
925impl OtherState {
926 pub fn state_key(&self) -> &str {
928 &self.state_key
929 }
930
931 pub fn content(&self) -> &AnyOtherStateEventContentChange {
933 &self.content
934 }
935
936 fn redact(&self, rules: &RedactionRules) -> Self {
937 Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
938 }
939}
940
941#[cfg(test)]
942mod tests {
943 use assert_matches2::assert_let;
944 use matrix_sdk_test::ALICE;
945 use ruma::{
946 assign,
947 events::{
948 StateEventContentChange,
949 room::member::{
950 MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
951 },
952 },
953 room_version_rules::RedactionRules,
954 };
955
956 use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
957
958 #[test]
959 fn redact_membership_change() {
960 let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
961 user_id: ALICE.to_owned(),
962 content: StateEventContentChange::Original {
963 content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
964 reason: Some("🤬".to_owned()),
965 }),
966 prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
967 MembershipState::Join,
968 )),
969 },
970 change: Some(MembershipChange::Banned),
971 });
972
973 let redacted = content.redact(&RedactionRules::V11);
974 assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
975 assert_eq!(inner.change, Some(MembershipChange::Banned));
976 assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
977 assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
978 }
979}