Skip to main content

matrix_sdk_ui/timeline/event_item/content/
mod.rs

1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::sync::Arc;
16
17use as_variant::as_variant;
18use matrix_sdk_base::crypto::types::events::UtdCause;
19use ruma::{
20    OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, UserId,
21    events::{
22        AnyStateEventContentChange, Mentions, MessageLikeEventType, StateEventContentChange,
23        StateEventType,
24        policy::rule::{
25            room::PolicyRuleRoomEventContent, server::PolicyRuleServerEventContent,
26            user::PolicyRuleUserEventContent,
27        },
28        room::{
29            aliases::RoomAliasesEventContent,
30            avatar::RoomAvatarEventContent,
31            canonical_alias::RoomCanonicalAliasEventContent,
32            create::RoomCreateEventContent,
33            encrypted::{EncryptedEventScheme, MegolmV1AesSha2Content, RoomEncryptedEventContent},
34            encryption::RoomEncryptionEventContent,
35            guest_access::RoomGuestAccessEventContent,
36            history_visibility::RoomHistoryVisibilityEventContent,
37            join_rules::RoomJoinRulesEventContent,
38            member::{Change, RoomMemberEventContent},
39            message::MessageType,
40            name::RoomNameEventContent,
41            pinned_events::RoomPinnedEventsEventContent,
42            power_levels::RoomPowerLevelsEventContent,
43            server_acl::RoomServerAclEventContent,
44            third_party_invite::RoomThirdPartyInviteEventContent,
45            tombstone::RoomTombstoneEventContent,
46            topic::RoomTopicEventContent,
47        },
48        space::{child::SpaceChildEventContent, parent::SpaceParentEventContent},
49        sticker::StickerEventContent,
50    },
51    html::RemoveReplyFallback,
52    room_version_rules::RedactionRules,
53};
54
55mod live_location;
56mod message;
57mod msg_like;
58pub(super) mod other;
59pub(crate) mod pinned_events;
60mod polls;
61mod reply;
62
63pub use pinned_events::RoomPinnedEventsChange;
64
65pub(in crate::timeline) use self::{
66    live_location::beacon_info_matches,
67    message::{
68        extract_bundled_edit_event_json, extract_poll_edit_content, extract_room_msg_edit_content,
69    },
70};
71pub use self::{
72    live_location::{BeaconInfo, LiveLocationState},
73    message::Message,
74    msg_like::{MsgLikeContent, MsgLikeKind, ThreadSummary},
75    other::OtherMessageLike,
76    polls::{PollResult, PollState},
77    reply::{EmbeddedEvent, InReplyToDetails},
78};
79use super::ReactionsByKeyBySender;
80
81/// The content of an [`EventTimelineItem`][super::EventTimelineItem].
82#[allow(clippy::large_enum_variant)]
83#[derive(Clone, Debug)]
84pub enum TimelineItemContent {
85    MsgLike(MsgLikeContent),
86
87    /// A room membership change.
88    MembershipChange(RoomMembershipChange),
89
90    /// A room member profile change.
91    ProfileChange(MemberProfileChange),
92
93    /// Another state event.
94    OtherState(OtherState),
95
96    /// A message-like event that failed to deserialize.
97    FailedToParseMessageLike {
98        /// The event `type`.
99        event_type: MessageLikeEventType,
100
101        /// The deserialization error.
102        error: Arc<serde_json::Error>,
103    },
104
105    /// A state event that failed to deserialize.
106    FailedToParseState {
107        /// The event `type`.
108        event_type: StateEventType,
109
110        /// The state key.
111        state_key: String,
112
113        /// The deserialization error.
114        error: Arc<serde_json::Error>,
115    },
116
117    /// An `m.call.invite` event
118    CallInvite,
119
120    /// An `m.rtc.notification` event
121    RtcNotification,
122}
123
124impl TimelineItemContent {
125    pub fn as_msglike(&self) -> Option<&MsgLikeContent> {
126        as_variant!(self, TimelineItemContent::MsgLike)
127    }
128
129    /// If `self` is of the [`MsgLike`][Self::MsgLike] variant with a
130    /// [`LiveLocation`][MsgLikeKind::LiveLocation] kind, return the inner
131    /// [`LiveLocationState`].
132    pub fn as_live_location_state(&self) -> Option<&LiveLocationState> {
133        as_variant!(self, Self::MsgLike(MsgLikeContent {
134            kind: MsgLikeKind::LiveLocation(state),
135            ..
136        }) => state)
137    }
138
139    /// If `self` is of the [`MsgLike`][Self::MsgLike] variant with a
140    /// [`LiveLocation`][MsgLikeKind::LiveLocation] kind, return the inner
141    /// [`LiveLocationState`] mutably.
142    pub(in crate::timeline) fn as_live_location_state_mut(
143        &mut self,
144    ) -> Option<&mut LiveLocationState> {
145        as_variant!(self, Self::MsgLike(MsgLikeContent {
146            kind: MsgLikeKind::LiveLocation(state),
147            ..
148        }) => state)
149    }
150
151    /// Check whether this item's content is a
152    /// [`LiveLocation`][MsgLikeKind::LiveLocation].
153    pub fn is_live_location(&self) -> bool {
154        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::LiveLocation(_), .. }))
155    }
156
157    /// If `self` is of the [`MsgLike`][Self::MsgLike] variant, return the
158    /// inner [`Message`].
159    pub fn as_message(&self) -> Option<&Message> {
160        as_variant!(self, Self::MsgLike(MsgLikeContent {
161            kind: MsgLikeKind::Message(message),
162            ..
163        }) => message)
164    }
165
166    /// Check whether this item's content is a
167    /// [`Message`][MsgLikeKind::Message].
168    pub fn is_message(&self) -> bool {
169        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Message(_), .. }))
170    }
171
172    /// If `self` is of the [`MsgLike`][Self::MsgLike] variant, return the
173    /// inner [`PollState`].
174    pub fn as_poll(&self) -> Option<&PollState> {
175        as_variant!(self, Self::MsgLike(MsgLikeContent {
176            kind: MsgLikeKind::Poll(poll_state),
177            ..
178        }) => poll_state)
179    }
180
181    /// Check whether this item's content is a
182    /// [`Poll`][MsgLikeKind::Poll].
183    pub fn is_poll(&self) -> bool {
184        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Poll(_), .. }))
185    }
186
187    pub fn as_sticker(&self) -> Option<&Sticker> {
188        as_variant!(
189            self,
190            Self::MsgLike(MsgLikeContent {
191                kind: MsgLikeKind::Sticker(sticker),
192                ..
193            }) => sticker
194        )
195    }
196
197    /// Check whether this item's content is a
198    /// [`Sticker`][MsgLikeKind::Sticker].
199    pub fn is_sticker(&self) -> bool {
200        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Sticker(_), .. }))
201    }
202
203    /// If `self` is of the [`UnableToDecrypt`][MsgLikeKind::UnableToDecrypt]
204    /// variant, return the inner [`EncryptedMessage`].
205    pub fn as_unable_to_decrypt(&self) -> Option<&EncryptedMessage> {
206        as_variant!(
207            self,
208            Self::MsgLike(MsgLikeContent {
209                kind: MsgLikeKind::UnableToDecrypt(encrypted_message),
210                ..
211            }) => encrypted_message
212        )
213    }
214
215    /// Check whether this item's content is a
216    /// [`UnableToDecrypt`][MsgLikeKind::UnableToDecrypt].
217    pub fn is_unable_to_decrypt(&self) -> bool {
218        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::UnableToDecrypt(_), .. }))
219    }
220
221    pub fn is_redacted(&self) -> bool {
222        matches!(self, Self::MsgLike(MsgLikeContent { kind: MsgLikeKind::Redacted, .. }))
223    }
224
225    // These constructors could also be `From` implementations, but that would
226    // allow users to call them directly, which should not be supported
227    pub(crate) fn message(
228        msgtype: MessageType,
229        mentions: Option<Mentions>,
230        reactions: ReactionsByKeyBySender,
231        thread_root: Option<OwnedEventId>,
232        in_reply_to: Option<InReplyToDetails>,
233        thread_summary: Option<ThreadSummary>,
234    ) -> Self {
235        let remove_reply_fallback =
236            if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
237
238        Self::MsgLike(MsgLikeContent {
239            kind: MsgLikeKind::Message(Message::from_event(
240                msgtype,
241                mentions,
242                None,
243                remove_reply_fallback,
244            )),
245            reactions,
246            thread_root,
247            in_reply_to,
248            thread_summary,
249        })
250    }
251
252    #[cfg(not(tarpaulin_include))] // debug-logging functionality
253    pub(crate) fn debug_string(&self) -> &'static str {
254        match self {
255            TimelineItemContent::MsgLike(msglike) => msglike.debug_string(),
256            TimelineItemContent::MembershipChange(_) => "a membership change",
257            TimelineItemContent::ProfileChange(_) => "a profile change",
258            TimelineItemContent::OtherState(_) => "a state event",
259            TimelineItemContent::FailedToParseMessageLike { .. }
260            | TimelineItemContent::FailedToParseState { .. } => "an event that couldn't be parsed",
261            TimelineItemContent::CallInvite => "a call invite",
262            TimelineItemContent::RtcNotification => "a call notification",
263        }
264    }
265
266    pub(crate) fn room_member(
267        user_id: OwnedUserId,
268        full_content: StateEventContentChange<RoomMemberEventContent>,
269        sender: OwnedUserId,
270    ) -> Self {
271        use ruma::events::room::member::MembershipChange as MChange;
272        match &full_content {
273            StateEventContentChange::Original { content, prev_content } => {
274                let membership_change = content.membership_change(
275                    prev_content.as_ref().map(|c| c.details()),
276                    &sender,
277                    &user_id,
278                );
279
280                if let MChange::ProfileChanged { displayname_change, avatar_url_change } =
281                    membership_change
282                {
283                    Self::ProfileChange(MemberProfileChange {
284                        user_id,
285                        displayname_change: displayname_change.map(|c| Change {
286                            new: c.new.map(ToOwned::to_owned),
287                            old: c.old.map(ToOwned::to_owned),
288                        }),
289                        avatar_url_change: avatar_url_change.map(|c| Change {
290                            new: c.new.map(ToOwned::to_owned),
291                            old: c.old.map(ToOwned::to_owned),
292                        }),
293                    })
294                } else {
295                    let change = match membership_change {
296                        MChange::None => MembershipChange::None,
297                        MChange::Error => MembershipChange::Error,
298                        MChange::Joined => MembershipChange::Joined,
299                        MChange::Left => MembershipChange::Left,
300                        MChange::Banned => MembershipChange::Banned,
301                        MChange::Unbanned => MembershipChange::Unbanned,
302                        MChange::Kicked => MembershipChange::Kicked,
303                        MChange::Invited => MembershipChange::Invited,
304                        MChange::KickedAndBanned => MembershipChange::KickedAndBanned,
305                        MChange::InvitationAccepted => MembershipChange::InvitationAccepted,
306                        MChange::InvitationRejected => MembershipChange::InvitationRejected,
307                        MChange::InvitationRevoked => MembershipChange::InvitationRevoked,
308                        MChange::Knocked => MembershipChange::Knocked,
309                        MChange::KnockAccepted => MembershipChange::KnockAccepted,
310                        MChange::KnockRetracted => MembershipChange::KnockRetracted,
311                        MChange::KnockDenied => MembershipChange::KnockDenied,
312                        MChange::ProfileChanged { .. } => unreachable!(),
313                        _ => MembershipChange::NotImplemented,
314                    };
315
316                    Self::MembershipChange(RoomMembershipChange {
317                        user_id,
318                        content: full_content,
319                        change: Some(change),
320                    })
321                }
322            }
323            StateEventContentChange::Redacted(_) => Self::MembershipChange(RoomMembershipChange {
324                user_id,
325                content: full_content,
326                change: None,
327            }),
328        }
329    }
330
331    pub(in crate::timeline) fn redact(&self, rules: &RedactionRules) -> Self {
332        match self {
333            Self::MsgLike(_) | Self::CallInvite | Self::RtcNotification => {
334                TimelineItemContent::MsgLike(MsgLikeContent::redacted())
335            }
336            Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
337            Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
338            Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
339            Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
340        }
341    }
342
343    /// Event ID of the thread root, if this is a message in a thread.
344    pub fn thread_root(&self) -> Option<OwnedEventId> {
345        as_variant!(self, Self::MsgLike)?.thread_root.clone()
346    }
347
348    /// Get the event this message is replying to, if any.
349    pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
350        as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
351    }
352
353    /// Return the reactions, grouped by key and then by sender, for a given
354    /// content.
355    pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
356        match self {
357            TimelineItemContent::MsgLike(msglike) => Some(&msglike.reactions),
358
359            TimelineItemContent::MembershipChange(..)
360            | TimelineItemContent::ProfileChange(..)
361            | TimelineItemContent::OtherState(..)
362            | TimelineItemContent::FailedToParseMessageLike { .. }
363            | TimelineItemContent::FailedToParseState { .. }
364            | TimelineItemContent::CallInvite
365            | TimelineItemContent::RtcNotification => {
366                // No reactions for these kind of items.
367                None
368            }
369        }
370    }
371
372    /// Information about the thread this item is the root for.
373    pub fn thread_summary(&self) -> Option<ThreadSummary> {
374        as_variant!(self, Self::MsgLike)?.thread_summary.clone()
375    }
376
377    /// Return a mutable handle to the reactions of this item.
378    ///
379    /// See also [`Self::reactions()`] to explain the optional return type.
380    pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
381        match self {
382            TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
383
384            TimelineItemContent::MembershipChange(..)
385            | TimelineItemContent::ProfileChange(..)
386            | TimelineItemContent::OtherState(..)
387            | TimelineItemContent::FailedToParseMessageLike { .. }
388            | TimelineItemContent::FailedToParseState { .. }
389            | TimelineItemContent::CallInvite
390            | TimelineItemContent::RtcNotification => {
391                // No reactions for these kind of items.
392                None
393            }
394        }
395    }
396
397    pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
398        let mut cloned = self.clone();
399        if let Some(r) = cloned.reactions_mut() {
400            *r = reactions;
401        }
402        cloned
403    }
404}
405
406/// Metadata about an `m.room.encrypted` event that could not be decrypted.
407#[derive(Clone, Debug)]
408pub enum EncryptedMessage {
409    /// Metadata about an event using the `m.olm.v1.curve25519-aes-sha2`
410    /// algorithm.
411    OlmV1Curve25519AesSha2 {
412        /// The Curve25519 key of the sender.
413        sender_key: String,
414    },
415    /// Metadata about an event using the `m.megolm.v1.aes-sha2` algorithm.
416    MegolmV1AesSha2 {
417        /// The Curve25519 key of the sender.
418        #[deprecated = "this field should still be sent but should not be used when received"]
419        #[doc(hidden)] // Included for Debug formatting only
420        sender_key: Option<String>,
421
422        /// The ID of the sending device.
423        #[deprecated = "this field should still be sent but should not be used when received"]
424        #[doc(hidden)] // Included for Debug formatting only
425        device_id: Option<OwnedDeviceId>,
426
427        /// The ID of the session used to encrypt the message.
428        session_id: String,
429
430        /// What we know about what caused this UTD. E.g. was this event sent
431        /// when we were not a member of this room?
432        cause: UtdCause,
433    },
434    /// No metadata because the event uses an unknown algorithm.
435    Unknown,
436}
437
438impl EncryptedMessage {
439    pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
440        match content.scheme {
441            EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
442                Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
443            }
444            #[allow(deprecated)]
445            EncryptedEventScheme::MegolmV1AesSha2(s) => {
446                let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
447
448                Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
449            }
450            _ => Self::Unknown,
451        }
452    }
453
454    /// Return the ID of the Megolm session used to encrypt this message, if it
455    /// was received via a Megolm session.
456    pub(crate) fn session_id(&self) -> Option<&str> {
457        match self {
458            EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
459            EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
460            EncryptedMessage::Unknown => None,
461        }
462    }
463}
464
465/// An `m.sticker` event.
466#[derive(Clone, Debug)]
467pub struct Sticker {
468    pub(in crate::timeline) content: StickerEventContent,
469}
470
471impl Sticker {
472    /// Get the data of this sticker.
473    pub fn content(&self) -> &StickerEventContent {
474        &self.content
475    }
476}
477
478/// An event changing a room membership.
479#[derive(Clone, Debug)]
480pub struct RoomMembershipChange {
481    pub(in crate::timeline) user_id: OwnedUserId,
482    pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
483    pub(in crate::timeline) change: Option<MembershipChange>,
484}
485
486impl RoomMembershipChange {
487    /// The ID of the user whose membership changed.
488    pub fn user_id(&self) -> &UserId {
489        &self.user_id
490    }
491
492    /// The full content of the event.
493    pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
494        &self.content
495    }
496
497    /// Retrieve the member's display name from the current event, or, if
498    /// missing, from the one it replaced.
499    pub fn display_name(&self) -> Option<String> {
500        if let StateEventContentChange::Original { content, prev_content } = &self.content {
501            content
502                .displayname
503                .as_ref()
504                .or_else(|| {
505                    prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
506                })
507                .cloned()
508        } else {
509            None
510        }
511    }
512
513    /// Retrieve the avatar URL from the current event, or, if missing, from the
514    /// one it replaced.
515    pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
516        if let StateEventContentChange::Original { content, prev_content } = &self.content {
517            content
518                .avatar_url
519                .as_ref()
520                .or_else(|| {
521                    prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
522                })
523                .cloned()
524        } else {
525            None
526        }
527    }
528
529    /// The membership change induced by this event.
530    ///
531    /// If this returns `None`, it doesn't mean that there was no change, but
532    /// that the change could not be computed. This is currently always the case
533    /// with redacted events.
534    // FIXME: Fetch the prev_content when missing so we can compute this with
535    // redacted events?
536    pub fn change(&self) -> Option<MembershipChange> {
537        self.change
538    }
539
540    fn redact(&self, rules: &RedactionRules) -> Self {
541        Self {
542            user_id: self.user_id.clone(),
543            content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
544            change: self.change,
545        }
546    }
547}
548
549/// An enum over all the possible room membership changes.
550#[derive(Clone, Copy, Debug, PartialEq, Eq)]
551pub enum MembershipChange {
552    /// No change.
553    None,
554
555    /// Must never happen.
556    Error,
557
558    /// User joined the room.
559    Joined,
560
561    /// User left the room.
562    Left,
563
564    /// User was banned.
565    Banned,
566
567    /// User was unbanned.
568    Unbanned,
569
570    /// User was kicked.
571    Kicked,
572
573    /// User was invited.
574    Invited,
575
576    /// User was kicked and banned.
577    KickedAndBanned,
578
579    /// User accepted the invite.
580    InvitationAccepted,
581
582    /// User rejected the invite.
583    InvitationRejected,
584
585    /// User had their invite revoked.
586    InvitationRevoked,
587
588    /// User knocked.
589    Knocked,
590
591    /// User had their knock accepted.
592    KnockAccepted,
593
594    /// User retracted their knock.
595    KnockRetracted,
596
597    /// User had their knock denied.
598    KnockDenied,
599
600    /// Not implemented.
601    NotImplemented,
602}
603
604/// An event changing a member's profile.
605///
606/// Note that profile changes only occur in the timeline when the user's
607/// membership is already `join`.
608#[derive(Clone, Debug)]
609pub struct MemberProfileChange {
610    pub(in crate::timeline) user_id: OwnedUserId,
611    pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
612    pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
613}
614
615impl MemberProfileChange {
616    /// The ID of the user whose profile changed.
617    pub fn user_id(&self) -> &UserId {
618        &self.user_id
619    }
620
621    /// The display name change induced by this event.
622    pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
623        self.displayname_change.as_ref()
624    }
625
626    /// The avatar URL change induced by this event.
627    pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
628        self.avatar_url_change.as_ref()
629    }
630
631    fn redact(&self) -> Self {
632        Self {
633            user_id: self.user_id.clone(),
634            // FIXME: This isn't actually right, the profile is reset to an
635            // empty one when the member event is redacted. This can't be
636            // implemented without further architectural changes and is a
637            // somewhat rare edge case, so it should be fine for now.
638            displayname_change: None,
639            avatar_url_change: None,
640        }
641    }
642}
643
644/// An enum over all the full state event contents that don't have their own
645/// `TimelineItemContent` variant.
646#[derive(Clone, Debug)]
647pub enum AnyOtherStateEventContentChange {
648    /// m.policy.rule.room
649    PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
650
651    /// m.policy.rule.server
652    PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
653
654    /// m.policy.rule.user
655    PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
656
657    /// m.room.aliases
658    RoomAliases(StateEventContentChange<RoomAliasesEventContent>),
659
660    /// m.room.avatar
661    RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
662
663    /// m.room.canonical_alias
664    RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
665
666    /// m.room.create
667    RoomCreate(StateEventContentChange<RoomCreateEventContent>),
668
669    /// m.room.encryption
670    RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
671
672    /// m.room.guest_access
673    RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
674
675    /// m.room.history_visibility
676    RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
677
678    /// m.room.join_rules
679    RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
680
681    /// m.room.name
682    RoomName(StateEventContentChange<RoomNameEventContent>),
683
684    /// m.room.pinned_events
685    RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
686
687    /// m.room.power_levels
688    RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
689
690    /// m.room.server_acl
691    RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
692
693    /// m.room.third_party_invite
694    RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
695
696    /// m.room.tombstone
697    RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
698
699    /// m.room.topic
700    RoomTopic(StateEventContentChange<RoomTopicEventContent>),
701
702    /// m.space.child
703    SpaceChild(StateEventContentChange<SpaceChildEventContent>),
704
705    /// m.space.parent
706    SpaceParent(StateEventContentChange<SpaceParentEventContent>),
707
708    #[doc(hidden)]
709    _Custom { event_type: String },
710}
711
712impl AnyOtherStateEventContentChange {
713    /// Create an `AnyOtherStateEventContentChange` from an
714    /// `AnyStateEventContentChange`.
715    ///
716    /// Panics if the event content does not match one of the variants.
717    // This could be a `From` implementation but we don't want it in the public API.
718    pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
719        let event_type = content.event_type();
720
721        match content {
722            AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
723            AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
724            AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
725            AnyStateEventContentChange::RoomAliases(c) => Self::RoomAliases(c),
726            AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
727            AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
728            AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
729            AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
730            AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
731            AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
732            AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
733            AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
734            AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
735            AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
736            AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
737            AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
738            AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
739            AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
740            AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
741            AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
742            AnyStateEventContentChange::RoomMember(_) => unreachable!(),
743            _ => Self::_Custom { event_type: event_type.to_string() },
744        }
745    }
746
747    /// Get the event's type, like `m.room.create`.
748    pub fn event_type(&self) -> StateEventType {
749        match self {
750            Self::PolicyRuleRoom(c) => c.event_type(),
751            Self::PolicyRuleServer(c) => c.event_type(),
752            Self::PolicyRuleUser(c) => c.event_type(),
753            Self::RoomAliases(c) => c.event_type(),
754            Self::RoomAvatar(c) => c.event_type(),
755            Self::RoomCanonicalAlias(c) => c.event_type(),
756            Self::RoomCreate(c) => c.event_type(),
757            Self::RoomEncryption(c) => c.event_type(),
758            Self::RoomGuestAccess(c) => c.event_type(),
759            Self::RoomHistoryVisibility(c) => c.event_type(),
760            Self::RoomJoinRules(c) => c.event_type(),
761            Self::RoomName(c) => c.event_type(),
762            Self::RoomPinnedEvents(c) => c.event_type(),
763            Self::RoomPowerLevels(c) => c.event_type(),
764            Self::RoomServerAcl(c) => c.event_type(),
765            Self::RoomThirdPartyInvite(c) => c.event_type(),
766            Self::RoomTombstone(c) => c.event_type(),
767            Self::RoomTopic(c) => c.event_type(),
768            Self::SpaceChild(c) => c.event_type(),
769            Self::SpaceParent(c) => c.event_type(),
770            Self::_Custom { event_type } => event_type.as_str().into(),
771        }
772    }
773
774    fn redact(&self, rules: &RedactionRules) -> Self {
775        match self {
776            Self::PolicyRuleRoom(c) => {
777                Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
778            }
779            Self::PolicyRuleServer(c) => {
780                Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
781            }
782            Self::PolicyRuleUser(c) => {
783                Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
784            }
785            Self::RoomAliases(c) => {
786                Self::RoomAliases(StateEventContentChange::Redacted(c.clone().redact(rules)))
787            }
788            Self::RoomAvatar(c) => {
789                Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
790            }
791            Self::RoomCanonicalAlias(c) => {
792                Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
793            }
794            Self::RoomCreate(c) => {
795                Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
796            }
797            Self::RoomEncryption(c) => {
798                Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
799            }
800            Self::RoomGuestAccess(c) => {
801                Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
802            }
803            Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
804                StateEventContentChange::Redacted(c.clone().redact(rules)),
805            ),
806            Self::RoomJoinRules(c) => {
807                Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
808            }
809            Self::RoomName(c) => {
810                Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
811            }
812            Self::RoomPinnedEvents(c) => {
813                Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
814            }
815            Self::RoomPowerLevels(c) => {
816                Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
817            }
818            Self::RoomServerAcl(c) => {
819                Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
820            }
821            Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
822                StateEventContentChange::Redacted(c.clone().redact(rules)),
823            ),
824            Self::RoomTombstone(c) => {
825                Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
826            }
827            Self::RoomTopic(c) => {
828                Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
829            }
830            Self::SpaceChild(c) => {
831                Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
832            }
833            Self::SpaceParent(c) => {
834                Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
835            }
836            Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
837        }
838    }
839}
840
841/// A state event that doesn't have its own variant.
842#[derive(Clone, Debug)]
843pub struct OtherState {
844    pub(in crate::timeline) state_key: String,
845    pub(in crate::timeline) content: AnyOtherStateEventContentChange,
846}
847
848impl OtherState {
849    /// The state key of the event.
850    pub fn state_key(&self) -> &str {
851        &self.state_key
852    }
853
854    /// The content of the event.
855    pub fn content(&self) -> &AnyOtherStateEventContentChange {
856        &self.content
857    }
858
859    fn redact(&self, rules: &RedactionRules) -> Self {
860        Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use assert_matches2::assert_let;
867    use matrix_sdk_test::ALICE;
868    use ruma::{
869        assign,
870        events::{
871            StateEventContentChange,
872            room::member::{
873                MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
874            },
875        },
876        room_version_rules::RedactionRules,
877    };
878
879    use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
880
881    #[test]
882    fn redact_membership_change() {
883        let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
884            user_id: ALICE.to_owned(),
885            content: StateEventContentChange::Original {
886                content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
887                    reason: Some("🤬".to_owned()),
888                }),
889                prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
890                    MembershipState::Join,
891                )),
892            },
893            change: Some(MembershipChange::Banned),
894        });
895
896        let redacted = content.redact(&RedactionRules::V11);
897        assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
898        assert_eq!(inner.change, Some(MembershipChange::Banned));
899        assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
900        assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
901    }
902}