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