Skip to main content

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