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