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