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