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(_) | Self::CallInvite | Self::RtcNotification { .. } => {
444                TimelineItemContent::MsgLike(MsgLikeContent::redacted())
445            }
446            Self::MembershipChange(ev) => Self::MembershipChange(ev.redact(rules)),
447            Self::ProfileChange(ev) => Self::ProfileChange(ev.redact()),
448            Self::OtherState(ev) => Self::OtherState(ev.redact(rules)),
449            Self::FailedToParseMessageLike { .. } | Self::FailedToParseState { .. } => self.clone(),
450        }
451    }
452
453    /// Event ID of the thread root, if this is a message in a thread.
454    pub fn thread_root(&self) -> Option<OwnedEventId> {
455        as_variant!(self, Self::MsgLike)?.thread_root.clone()
456    }
457
458    /// Get the event this message is replying to, if any.
459    pub fn in_reply_to(&self) -> Option<InReplyToDetails> {
460        as_variant!(self, Self::MsgLike)?.in_reply_to.clone()
461    }
462
463    /// Return the reactions, grouped by key and then by sender, for a given
464    /// content.
465    pub fn reactions(&self) -> Option<&ReactionsByKeyBySender> {
466        match self {
467            TimelineItemContent::MsgLike(msglike) => Some(&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    /// Information about the thread this item is the root for.
483    pub fn thread_summary(&self) -> Option<ThreadSummary> {
484        as_variant!(self, Self::MsgLike)?.thread_summary.clone()
485    }
486
487    /// Return a mutable handle to the reactions of this item.
488    ///
489    /// See also [`Self::reactions()`] to explain the optional return type.
490    pub(crate) fn reactions_mut(&mut self) -> Option<&mut ReactionsByKeyBySender> {
491        match self {
492            TimelineItemContent::MsgLike(msglike) => Some(&mut msglike.reactions),
493
494            TimelineItemContent::MembershipChange(..)
495            | TimelineItemContent::ProfileChange(..)
496            | TimelineItemContent::OtherState(..)
497            | TimelineItemContent::FailedToParseMessageLike { .. }
498            | TimelineItemContent::FailedToParseState { .. }
499            | TimelineItemContent::CallInvite
500            | TimelineItemContent::RtcNotification { .. } => {
501                // No reactions for these kind of items.
502                None
503            }
504        }
505    }
506
507    pub fn with_reactions(&self, reactions: ReactionsByKeyBySender) -> Self {
508        let mut cloned = self.clone();
509        if let Some(r) = cloned.reactions_mut() {
510            *r = reactions;
511        }
512        cloned
513    }
514}
515
516/// Metadata about an `m.room.encrypted` event that could not be decrypted.
517#[derive(Clone, Debug)]
518pub enum EncryptedMessage {
519    /// Metadata about an event using the `m.olm.v1.curve25519-aes-sha2`
520    /// algorithm.
521    OlmV1Curve25519AesSha2 {
522        /// The Curve25519 key of the sender.
523        sender_key: String,
524    },
525    /// Metadata about an event using the `m.megolm.v1.aes-sha2` algorithm.
526    MegolmV1AesSha2 {
527        /// The Curve25519 key of the sender.
528        #[deprecated = "this field should still be sent but should not be used when received"]
529        #[doc(hidden)] // Included for Debug formatting only
530        sender_key: Option<String>,
531
532        /// The ID of the sending device.
533        #[deprecated = "this field should still be sent but should not be used when received"]
534        #[doc(hidden)] // Included for Debug formatting only
535        device_id: Option<OwnedDeviceId>,
536
537        /// The ID of the session used to encrypt the message.
538        session_id: String,
539
540        /// What we know about what caused this UTD. E.g. was this event sent
541        /// when we were not a member of this room?
542        cause: UtdCause,
543    },
544    /// No metadata because the event uses an unknown algorithm.
545    Unknown,
546}
547
548impl EncryptedMessage {
549    pub(crate) fn from_content(content: RoomEncryptedEventContent, cause: UtdCause) -> Self {
550        match content.scheme {
551            EncryptedEventScheme::OlmV1Curve25519AesSha2(s) => {
552                Self::OlmV1Curve25519AesSha2 { sender_key: s.sender_key }
553            }
554            #[allow(deprecated)]
555            EncryptedEventScheme::MegolmV1AesSha2(s) => {
556                let MegolmV1AesSha2Content { sender_key, device_id, session_id, .. } = s;
557
558                Self::MegolmV1AesSha2 { sender_key, device_id, session_id, cause }
559            }
560            _ => Self::Unknown,
561        }
562    }
563
564    /// Return the ID of the Megolm session used to encrypt this message, if it
565    /// was received via a Megolm session.
566    pub(crate) fn session_id(&self) -> Option<&str> {
567        match self {
568            EncryptedMessage::OlmV1Curve25519AesSha2 { .. } => None,
569            EncryptedMessage::MegolmV1AesSha2 { session_id, .. } => Some(session_id),
570            EncryptedMessage::Unknown => None,
571        }
572    }
573}
574
575/// An `m.sticker` event.
576#[derive(Clone, Debug)]
577pub struct Sticker {
578    pub(in crate::timeline) content: StickerEventContent,
579}
580
581impl Sticker {
582    /// Get the data of this sticker.
583    pub fn content(&self) -> &StickerEventContent {
584        &self.content
585    }
586}
587
588/// An event changing a room membership.
589#[derive(Clone, Debug)]
590pub struct RoomMembershipChange {
591    pub(in crate::timeline) user_id: OwnedUserId,
592    pub(in crate::timeline) content: StateEventContentChange<RoomMemberEventContent>,
593    pub(in crate::timeline) change: Option<MembershipChange>,
594}
595
596impl RoomMembershipChange {
597    /// The ID of the user whose membership changed.
598    pub fn user_id(&self) -> &UserId {
599        &self.user_id
600    }
601
602    /// The full content of the event.
603    pub fn content(&self) -> &StateEventContentChange<RoomMemberEventContent> {
604        &self.content
605    }
606
607    /// Retrieve the member's display name from the current event, or, if
608    /// missing, from the one it replaced.
609    pub fn display_name(&self) -> Option<String> {
610        if let StateEventContentChange::Original { content, prev_content } = &self.content {
611            content
612                .displayname
613                .as_ref()
614                .or_else(|| {
615                    prev_content.as_ref().and_then(|prev_content| prev_content.displayname.as_ref())
616                })
617                .cloned()
618        } else {
619            None
620        }
621    }
622
623    /// Retrieve the avatar URL from the current event, or, if missing, from the
624    /// one it replaced.
625    pub fn avatar_url(&self) -> Option<OwnedMxcUri> {
626        if let StateEventContentChange::Original { content, prev_content } = &self.content {
627            content
628                .avatar_url
629                .as_ref()
630                .or_else(|| {
631                    prev_content.as_ref().and_then(|prev_content| prev_content.avatar_url.as_ref())
632                })
633                .cloned()
634        } else {
635            None
636        }
637    }
638
639    /// The membership change induced by this event.
640    ///
641    /// If this returns `None`, it doesn't mean that there was no change, but
642    /// that the change could not be computed. This is currently always the case
643    /// with redacted events.
644    // FIXME: Fetch the prev_content when missing so we can compute this with
645    // redacted events?
646    pub fn change(&self) -> Option<MembershipChange> {
647        self.change
648    }
649
650    fn redact(&self, rules: &RedactionRules) -> Self {
651        Self {
652            user_id: self.user_id.clone(),
653            content: StateEventContentChange::Redacted(self.content.clone().redact(rules)),
654            change: self.change,
655        }
656    }
657}
658
659/// An enum over all the possible room membership changes.
660#[derive(Clone, Copy, Debug, PartialEq, Eq)]
661pub enum MembershipChange {
662    /// No change.
663    None,
664
665    /// Must never happen.
666    Error,
667
668    /// User joined the room.
669    Joined,
670
671    /// User left the room.
672    Left,
673
674    /// User was banned.
675    Banned,
676
677    /// User was unbanned.
678    Unbanned,
679
680    /// User was kicked.
681    Kicked,
682
683    /// User was invited.
684    Invited,
685
686    /// User was kicked and banned.
687    KickedAndBanned,
688
689    /// User accepted the invite.
690    InvitationAccepted,
691
692    /// User rejected the invite.
693    InvitationRejected,
694
695    /// User had their invite revoked.
696    InvitationRevoked,
697
698    /// User knocked.
699    Knocked,
700
701    /// User had their knock accepted.
702    KnockAccepted,
703
704    /// User retracted their knock.
705    KnockRetracted,
706
707    /// User had their knock denied.
708    KnockDenied,
709
710    /// Not implemented.
711    NotImplemented,
712}
713
714/// An event changing a member's profile.
715///
716/// Note that profile changes only occur in the timeline when the user's
717/// membership is already `join`.
718#[derive(Clone, Debug)]
719pub struct MemberProfileChange {
720    pub(in crate::timeline) user_id: OwnedUserId,
721    pub(in crate::timeline) displayname_change: Option<Change<Option<String>>>,
722    pub(in crate::timeline) avatar_url_change: Option<Change<Option<OwnedMxcUri>>>,
723}
724
725impl MemberProfileChange {
726    /// The ID of the user whose profile changed.
727    pub fn user_id(&self) -> &UserId {
728        &self.user_id
729    }
730
731    /// The display name change induced by this event.
732    pub fn displayname_change(&self) -> Option<&Change<Option<String>>> {
733        self.displayname_change.as_ref()
734    }
735
736    /// The avatar URL change induced by this event.
737    pub fn avatar_url_change(&self) -> Option<&Change<Option<OwnedMxcUri>>> {
738        self.avatar_url_change.as_ref()
739    }
740
741    fn redact(&self) -> Self {
742        Self {
743            user_id: self.user_id.clone(),
744            // FIXME: This isn't actually right, the profile is reset to an
745            // empty one when the member event is redacted. This can't be
746            // implemented without further architectural changes and is a
747            // somewhat rare edge case, so it should be fine for now.
748            displayname_change: None,
749            avatar_url_change: None,
750        }
751    }
752}
753
754/// An enum over all the full state event contents that don't have their own
755/// `TimelineItemContent` variant.
756#[derive(Clone, Debug)]
757pub enum AnyOtherStateEventContentChange {
758    /// m.policy.rule.room
759    PolicyRuleRoom(StateEventContentChange<PolicyRuleRoomEventContent>),
760
761    /// m.policy.rule.server
762    PolicyRuleServer(StateEventContentChange<PolicyRuleServerEventContent>),
763
764    /// m.policy.rule.user
765    PolicyRuleUser(StateEventContentChange<PolicyRuleUserEventContent>),
766
767    /// m.room.avatar
768    RoomAvatar(StateEventContentChange<RoomAvatarEventContent>),
769
770    /// m.room.canonical_alias
771    RoomCanonicalAlias(StateEventContentChange<RoomCanonicalAliasEventContent>),
772
773    /// m.room.create
774    RoomCreate(StateEventContentChange<RoomCreateEventContent>),
775
776    /// m.room.encryption
777    RoomEncryption(StateEventContentChange<RoomEncryptionEventContent>),
778
779    /// m.room.guest_access
780    RoomGuestAccess(StateEventContentChange<RoomGuestAccessEventContent>),
781
782    /// m.room.history_visibility
783    RoomHistoryVisibility(StateEventContentChange<RoomHistoryVisibilityEventContent>),
784
785    /// m.room.join_rules
786    RoomJoinRules(StateEventContentChange<RoomJoinRulesEventContent>),
787
788    /// m.room.name
789    RoomName(StateEventContentChange<RoomNameEventContent>),
790
791    /// m.room.pinned_events
792    RoomPinnedEvents(StateEventContentChange<RoomPinnedEventsEventContent>),
793
794    /// m.room.power_levels
795    RoomPowerLevels(StateEventContentChange<RoomPowerLevelsEventContent>),
796
797    /// m.room.server_acl
798    RoomServerAcl(StateEventContentChange<RoomServerAclEventContent>),
799
800    /// m.room.third_party_invite
801    RoomThirdPartyInvite(StateEventContentChange<RoomThirdPartyInviteEventContent>),
802
803    /// m.room.tombstone
804    RoomTombstone(StateEventContentChange<RoomTombstoneEventContent>),
805
806    /// m.room.topic
807    RoomTopic(StateEventContentChange<RoomTopicEventContent>),
808
809    /// m.space.child
810    SpaceChild(StateEventContentChange<SpaceChildEventContent>),
811
812    /// m.space.parent
813    SpaceParent(StateEventContentChange<SpaceParentEventContent>),
814
815    #[doc(hidden)]
816    _Custom { event_type: String },
817}
818
819impl AnyOtherStateEventContentChange {
820    /// Create an `AnyOtherStateEventContentChange` from an
821    /// `AnyStateEventContentChange`.
822    ///
823    /// Panics if the event content does not match one of the variants.
824    // This could be a `From` implementation but we don't want it in the public API.
825    pub(crate) fn with_event_content(content: AnyStateEventContentChange) -> Self {
826        let event_type = content.event_type();
827
828        match content {
829            AnyStateEventContentChange::PolicyRuleRoom(c) => Self::PolicyRuleRoom(c),
830            AnyStateEventContentChange::PolicyRuleServer(c) => Self::PolicyRuleServer(c),
831            AnyStateEventContentChange::PolicyRuleUser(c) => Self::PolicyRuleUser(c),
832            AnyStateEventContentChange::RoomAvatar(c) => Self::RoomAvatar(c),
833            AnyStateEventContentChange::RoomCanonicalAlias(c) => Self::RoomCanonicalAlias(c),
834            AnyStateEventContentChange::RoomCreate(c) => Self::RoomCreate(c),
835            AnyStateEventContentChange::RoomEncryption(c) => Self::RoomEncryption(c),
836            AnyStateEventContentChange::RoomGuestAccess(c) => Self::RoomGuestAccess(c),
837            AnyStateEventContentChange::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(c),
838            AnyStateEventContentChange::RoomJoinRules(c) => Self::RoomJoinRules(c),
839            AnyStateEventContentChange::RoomName(c) => Self::RoomName(c),
840            AnyStateEventContentChange::RoomPinnedEvents(c) => Self::RoomPinnedEvents(c),
841            AnyStateEventContentChange::RoomPowerLevels(c) => Self::RoomPowerLevels(c),
842            AnyStateEventContentChange::RoomServerAcl(c) => Self::RoomServerAcl(c),
843            AnyStateEventContentChange::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(c),
844            AnyStateEventContentChange::RoomTombstone(c) => Self::RoomTombstone(c),
845            AnyStateEventContentChange::RoomTopic(c) => Self::RoomTopic(c),
846            AnyStateEventContentChange::SpaceChild(c) => Self::SpaceChild(c),
847            AnyStateEventContentChange::SpaceParent(c) => Self::SpaceParent(c),
848            AnyStateEventContentChange::RoomMember(_) => unreachable!(),
849            _ => Self::_Custom { event_type: event_type.to_string() },
850        }
851    }
852
853    /// Get the event's type, like `m.room.create`.
854    pub fn event_type(&self) -> StateEventType {
855        match self {
856            Self::PolicyRuleRoom(c) => c.event_type(),
857            Self::PolicyRuleServer(c) => c.event_type(),
858            Self::PolicyRuleUser(c) => c.event_type(),
859            Self::RoomAvatar(c) => c.event_type(),
860            Self::RoomCanonicalAlias(c) => c.event_type(),
861            Self::RoomCreate(c) => c.event_type(),
862            Self::RoomEncryption(c) => c.event_type(),
863            Self::RoomGuestAccess(c) => c.event_type(),
864            Self::RoomHistoryVisibility(c) => c.event_type(),
865            Self::RoomJoinRules(c) => c.event_type(),
866            Self::RoomName(c) => c.event_type(),
867            Self::RoomPinnedEvents(c) => c.event_type(),
868            Self::RoomPowerLevels(c) => c.event_type(),
869            Self::RoomServerAcl(c) => c.event_type(),
870            Self::RoomThirdPartyInvite(c) => c.event_type(),
871            Self::RoomTombstone(c) => c.event_type(),
872            Self::RoomTopic(c) => c.event_type(),
873            Self::SpaceChild(c) => c.event_type(),
874            Self::SpaceParent(c) => c.event_type(),
875            Self::_Custom { event_type } => event_type.as_str().into(),
876        }
877    }
878
879    fn redact(&self, rules: &RedactionRules) -> Self {
880        match self {
881            Self::PolicyRuleRoom(c) => {
882                Self::PolicyRuleRoom(StateEventContentChange::Redacted(c.clone().redact(rules)))
883            }
884            Self::PolicyRuleServer(c) => {
885                Self::PolicyRuleServer(StateEventContentChange::Redacted(c.clone().redact(rules)))
886            }
887            Self::PolicyRuleUser(c) => {
888                Self::PolicyRuleUser(StateEventContentChange::Redacted(c.clone().redact(rules)))
889            }
890            Self::RoomAvatar(c) => {
891                Self::RoomAvatar(StateEventContentChange::Redacted(c.clone().redact(rules)))
892            }
893            Self::RoomCanonicalAlias(c) => {
894                Self::RoomCanonicalAlias(StateEventContentChange::Redacted(c.clone().redact(rules)))
895            }
896            Self::RoomCreate(c) => {
897                Self::RoomCreate(StateEventContentChange::Redacted(c.clone().redact(rules)))
898            }
899            Self::RoomEncryption(c) => {
900                Self::RoomEncryption(StateEventContentChange::Redacted(c.clone().redact(rules)))
901            }
902            Self::RoomGuestAccess(c) => {
903                Self::RoomGuestAccess(StateEventContentChange::Redacted(c.clone().redact(rules)))
904            }
905            Self::RoomHistoryVisibility(c) => Self::RoomHistoryVisibility(
906                StateEventContentChange::Redacted(c.clone().redact(rules)),
907            ),
908            Self::RoomJoinRules(c) => {
909                Self::RoomJoinRules(StateEventContentChange::Redacted(c.clone().redact(rules)))
910            }
911            Self::RoomName(c) => {
912                Self::RoomName(StateEventContentChange::Redacted(c.clone().redact(rules)))
913            }
914            Self::RoomPinnedEvents(c) => {
915                Self::RoomPinnedEvents(StateEventContentChange::Redacted(c.clone().redact(rules)))
916            }
917            Self::RoomPowerLevels(c) => {
918                Self::RoomPowerLevels(StateEventContentChange::Redacted(c.clone().redact(rules)))
919            }
920            Self::RoomServerAcl(c) => {
921                Self::RoomServerAcl(StateEventContentChange::Redacted(c.clone().redact(rules)))
922            }
923            Self::RoomThirdPartyInvite(c) => Self::RoomThirdPartyInvite(
924                StateEventContentChange::Redacted(c.clone().redact(rules)),
925            ),
926            Self::RoomTombstone(c) => {
927                Self::RoomTombstone(StateEventContentChange::Redacted(c.clone().redact(rules)))
928            }
929            Self::RoomTopic(c) => {
930                Self::RoomTopic(StateEventContentChange::Redacted(c.clone().redact(rules)))
931            }
932            Self::SpaceChild(c) => {
933                Self::SpaceChild(StateEventContentChange::Redacted(c.clone().redact(rules)))
934            }
935            Self::SpaceParent(c) => {
936                Self::SpaceParent(StateEventContentChange::Redacted(c.clone().redact(rules)))
937            }
938            Self::_Custom { event_type } => Self::_Custom { event_type: event_type.clone() },
939        }
940    }
941}
942
943/// A state event that doesn't have its own variant.
944#[derive(Clone, Debug)]
945pub struct OtherState {
946    pub(in crate::timeline) state_key: String,
947    pub(in crate::timeline) content: AnyOtherStateEventContentChange,
948}
949
950impl OtherState {
951    /// The state key of the event.
952    pub fn state_key(&self) -> &str {
953        &self.state_key
954    }
955
956    /// The content of the event.
957    pub fn content(&self) -> &AnyOtherStateEventContentChange {
958        &self.content
959    }
960
961    fn redact(&self, rules: &RedactionRules) -> Self {
962        Self { state_key: self.state_key.clone(), content: self.content.redact(rules) }
963    }
964}
965
966#[cfg(test)]
967mod tests {
968    use assert_matches2::assert_let;
969    use matrix_sdk_test::ALICE;
970    use ruma::{
971        assign,
972        events::{
973            StateEventContentChange,
974            room::member::{
975                MembershipState, PossiblyRedactedRoomMemberEventContent, RoomMemberEventContent,
976            },
977        },
978        room_version_rules::RedactionRules,
979    };
980
981    use super::{MembershipChange, RoomMembershipChange, TimelineItemContent};
982
983    #[test]
984    fn redact_membership_change() {
985        let content = TimelineItemContent::MembershipChange(RoomMembershipChange {
986            user_id: ALICE.to_owned(),
987            content: StateEventContentChange::Original {
988                content: assign!(RoomMemberEventContent::new(MembershipState::Ban), {
989                    reason: Some("🤬".to_owned()),
990                }),
991                prev_content: Some(PossiblyRedactedRoomMemberEventContent::new(
992                    MembershipState::Join,
993                )),
994            },
995            change: Some(MembershipChange::Banned),
996        });
997
998        let redacted = content.redact(&RedactionRules::V11);
999        assert_let!(TimelineItemContent::MembershipChange(inner) = redacted);
1000        assert_eq!(inner.change, Some(MembershipChange::Banned));
1001        assert_let!(StateEventContentChange::Redacted(inner_content_redacted) = inner.content);
1002        assert_eq!(inner_content_redacted.membership, MembershipState::Ban);
1003    }
1004}