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