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