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