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(msglike) => TimelineItemContent::MsgLike(MsgLikeContent {
444 kind: MsgLikeKind::Redacted,
445 reactions: Default::default(),
446 in_reply_to: None,
447 ..msglike.clone()
448 }),
449 Self::CallInvite | Self::RtcNotification { .. } => {
450 TimelineItemContent::MsgLike(MsgLikeContent::redacted())
451 }
452 Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
453 Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
454 Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
455 Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
456 }
457 }
458
459 pub fn thread_root(&self) -> Option<OwnedEventId> {
461 as_variant!(self, Self::MsgLike)?.thread_root.clone()
462 }
463
464 pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
466 as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
467 }
468
469 pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
472 match self {
473 TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
474
475 TimelineItemContent::MembershipChange(..)
476 | TimelineItemContent::ProfileChange(..)
477 | TimelineItemContent::OtherState(..)
478 | TimelineItemContent::FailedToParseMessageLike { .. }
479 | TimelineItemContent::FailedToParseState { .. }
480 | TimelineItemContent::CallInvite
481 | TimelineItemContent::RtcNotification { .. } => {
482 None
484 }
485 }
486 }
487
488 pub fn thread_summary(&self) -> Option<ThreadSummary> {
490 as_variant!(self, Self::MsgLike)?.thread_summary.clone()
491 }
492
493 pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
497 match self {
498 TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
499
500 TimelineItemContent::MembershipChange(..)
501 | TimelineItemContent::ProfileChange(..)
502 | TimelineItemContent::OtherState(..)
503 | TimelineItemContent::FailedToParseMessageLike { .. }
504 | TimelineItemContent::FailedToParseState { .. }
505 | TimelineItemContent::CallInvite
506 | TimelineItemContent::RtcNotification { .. } => {
507 None
509 }
510 }
511 }
512
513 pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
514 let mut cloned = self.clone();
515 if let Some(r) = cloned.reactions_mut() {
516 *r = reactions;
517 }
518 cloned
519 }
520}
521
522#[derive(Clone, Debug)]
524pub enum EncryptedMessage {
525 OlmV1Curve25519AesSha2 {
528 sender_key: String,
530 },
531 MegolmV1AesSha2 {
533 #[deprecated = "this field should still be sent but should not be used when received"]
535 #[doc(hidden)] sender_key: Option<String>,
537
538 #[deprecated = "this field should still be sent but should not be used when received"]
540 #[doc(hidden)] device_id: Option<OwnedDeviceId>,
542
543 session_id: String,
545
546 cause: UtdCause,
549 },
550 Unknown,
552}
553
554impl EncryptedMessage {
555 pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
556 match content.scheme {
557 EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
558 Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
559 }
560 #[allow(deprecated)]
561 EncryptedEventScheme::MegolmV1AesSha2(s) => {
562 let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
563
564 Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
565 }
566 _ => Self::Unknown,
567 }
568 }
569
570 pub(crate) fn session_id(&self) -> Option<&str> {
573 match self {
574 EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
575 EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
576 EncryptedMessage::Unknown => None,
577 }
578 }
579}
580
581#[derive(Clone, Debug)]
583pub struct Sticker {
584 pub(in crate::timeline) content: StickerEventContent,
585}
586
587impl Sticker {
588 pub fn content(&self) -> &StickerEventContent {
590 &self.content
591 }
592}
593
594#[derive(Clone, Debug)]
596pub struct RoomMembershipChange {
597 pub(in crate::timeline) user_id: OwnedUserId,
598 pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
599 pub(in crate::timeline) change: Option<MembershipChange>,
600}
601
602impl RoomMembershipChange {
603 pub fn user_id(&self) -> &UserId {
605 &self.user_id
606 }
607
608 pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
610 &self.content
611 }
612
613 pub fn display_name(&self) -> Option<String> {
616 if let StateEventContentChange::Original { content, prev_content } = &self.content {
617 content
618 .displayname
619 .as_ref()
620 .or_else(|| {
621 prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
622 })
623 .cloned()
624 } else {
625 None
626 }
627 }
628
629 pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
632 if let StateEventContentChange::Original { content, prev_content } = &self.content {
633 content
634 .avatar_url
635 .as_ref()
636 .or_else(|| {
637 prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
638 })
639 .cloned()
640 } else {
641 None
642 }
643 }
644
645 pub fn change(&self) -> Option<MembershipChange> {
653 self.change
654 }
655
656 fn redact(&self, rules: &RedactionRules) -> Self {
657 Self {
658 user_id: self.user_id.clone(),
659 content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
660 change: self.change,
661 }
662 }
663}
664
665#[derive(Clone, Copy, Debug, PartialEq, Eq)]
667pub enum MembershipChange {
668 None,
670
671 Error,
673
674 Joined,
676
677 Left,
679
680 Banned,
682
683 Unbanned,
685
686 Kicked,
688
689 Invited,
691
692 KickedAndBanned,
694
695 InvitationAccepted,
697
698 InvitationRejected,
700
701 InvitationRevoked,
703
704 Knocked,
706
707 KnockAccepted,
709
710 KnockRetracted,
712
713 KnockDenied,
715
716 NotImplemented,
718}
719
720#[derive(Clone, Debug)]
725pub struct MemberProfileChange {
726 pub(in crate::timeline) user_id: OwnedUserId,
727 pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
728 pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
729}
730
731impl MemberProfileChange {
732 pub fn user_id(&self) -> &UserId {
734 &self.user_id
735 }
736
737 pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
739 self.displayname_change.as_ref()
740 }
741
742 pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
744 self.avatar_url_change.as_ref()
745 }
746
747 fn redact(&self) -> Self {
748 Self {
749 user_id: self.user_id.clone(),
750 displayname_change: None,
755 avatar_url_change: None,
756 }
757 }
758}
759
760#[derive(Clone, Debug)]
763pub enum AnyOtherStateEventContentChange {
764 PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
766
767 PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
769
770 PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
772
773 RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
775
776 RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
778
779 RoomCreate(StateEventContentChange<RoomCreateEventContent>),
781
782 RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
784
785 RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
787
788 RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
790
791 RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
793
794 RoomName(StateEventContentChange<RoomNameEventContent>),
796
797 RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
799
800 RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
802
803 RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
805
806 RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
808
809 RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
811
812 RoomTopic(StateEventContentChange<RoomTopicEventContent>),
814
815 SpaceChild(StateEventContentChange<SpaceChildEventContent>),
817
818 SpaceParent(StateEventContentChange<SpaceParentEventContent>),
820
821 #[doc(hidden)]
822 _Custom { event_type: String },
823}
824
825impl AnyOtherStateEventContentChange {
826 pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
832 let event_type = content.event_type();
833
834 match content {
835 AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
836 AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
837 AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
838 AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
839 AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
840 AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
841 AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
842 AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
843 AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
844 AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
845 AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
846 AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
847 AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
848 AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
849 AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
850 AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
851 AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
852 AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
853 AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
854 AnyStateEventContentChange::RoomMember(_) => unreachable!(),
855 _ => Self::_Custom { event_type: event_type.to_string() },
856 }
857 }
858
859 pub fn event_type(&self) -> StateEventType {
861 match self {
862 Self::PolicyRuleRoom(c) => c.event_type(),
863 Self::PolicyRuleServer(c) => c.event_type(),
864 Self::PolicyRuleUser(c) => c.event_type(),
865 Self::RoomAvatar(c) => c.event_type(),
866 Self::RoomCanonicalAlias(c) => c.event_type(),
867 Self::RoomCreate(c) => c.event_type(),
868 Self::RoomEncryption(c) => c.event_type(),
869 Self::RoomGuestAccess(c) => c.event_type(),
870 Self::RoomHistoryVisibility(c) => c.event_type(),
871 Self::RoomJoinRules(c) => c.event_type(),
872 Self::RoomName(c) => c.event_type(),
873 Self::RoomPinnedEvents(c) => c.event_type(),
874 Self::RoomPowerLevels(c) => c.event_type(),
875 Self::RoomServerAcl(c) => c.event_type(),
876 Self::RoomThirdPartyInvite(c) => c.event_type(),
877 Self::RoomTombstone(c) => c.event_type(),
878 Self::RoomTopic(c) => c.event_type(),
879 Self::SpaceChild(c) => c.event_type(),
880 Self::SpaceParent(c) => c.event_type(),
881 Self::_Custom { event_type } => event_type.as_str().into(),
882 }
883 }
884
885 fn redact(&self, rules: &RedactionRules) -> Self {
886 match self {
887 Self::PolicyRuleRoom(c) => {
888 Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
889 }
890 Self::PolicyRuleServer(c) => {
891 Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
892 }
893 Self::PolicyRuleUser(c) => {
894 Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
895 }
896 Self::RoomAvatar(c) => {
897 Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
898 }
899 Self::RoomCanonicalAlias(c) => {
900 Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
901 }
902 Self::RoomCreate(c) => {
903 Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
904 }
905 Self::RoomEncryption(c) => {
906 Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
907 }
908 Self::RoomGuestAccess(c) => {
909 Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
910 }
911 Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
912 StateEventContentChange::Redacted(c.clone().redact(rules)),
913 ),
914 Self::RoomJoinRules(c) => {
915 Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
916 }
917 Self::RoomName(c) => {
918 Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
919 }
920 Self::RoomPinnedEvents(c) => {
921 Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
922 }
923 Self::RoomPowerLevels(c) => {
924 Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
925 }
926 Self::RoomServerAcl(c) => {
927 Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
928 }
929 Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
930 StateEventContentChange::Redacted(c.clone().redact(rules)),
931 ),
932 Self::RoomTombstone(c) => {
933 Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
934 }
935 Self::RoomTopic(c) => {
936 Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
937 }
938 Self::SpaceChild(c) => {
939 Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
940 }
941 Self::SpaceParent(c) => {
942 Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
943 }
944 Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
945 }
946 }
947}
948
949#[derive(Clone, Debug)]
951pub struct OtherState {
952 pub(in crate::timeline) state_key: String,
953 pub(in crate::timeline) content: AnyOtherStateEventContentChange,
954}
955
956impl OtherState {
957 pub fn state_key(&self) -> &str {
959 &self.state_key
960 }
961
962 pub fn content(&self) -> &AnyOtherStateEventContentChange {
964 &self.content
965 }
966
967 fn redact(&self, rules: &RedactionRules) -> Self {
968 Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
969 }
970}
971
972#[cfg(test)]
973mod tests {
974 use assert_matches2::assert_let;
975 use matrix_sdk_test::ALICE;
976 use ruma::{
977 assign,
978 events::{
979 StateEventContentChange,
980 room::member::{
981 MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
982 },
983 },
984 room_version_rules::RedactionRules,
985 };
986
987 use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
988
989 #[test]
990 fn redact_membership_change() {
991 let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
992 user_id: ALICE.to_owned(),
993 content: StateEventContentChange::Original {
994 content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
995 reason: Some("🤬".to_owned()),
996 }),
997 prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
998 MembershipState::Join,
999 )),
1000 },
1001 change: Some(MembershipChange::Banned),
1002 });
1003
1004 let redacted = content.redact(&RedactionRules::V11);
1005 assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
1006 assert_eq!(inner.change, Some(MembershipChange::Banned));
1007 assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
1008 assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
1009 }
1010}