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