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