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