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