Skip to main content

matrix_sdk_ui/timeline/event_item/content/
mod.rs

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