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