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