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