matrix_sdk_ui/timeline/event_item/content/
message.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
15//! Timeline item content bits for `m.room.message` events.
16
17use std::{fmt, sync::Arc};
18
19use imbl::{vector, Vector};
20use matrix_sdk::{
21    crypto::types::events::UtdCause,
22    deserialized_responses::{TimelineEvent, TimelineEventKind},
23    Room,
24};
25use ruma::{
26    assign,
27    events::{
28        poll::unstable_start::{
29            NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent,
30            UnstablePollStartEventContent,
31        },
32        relation::{InReplyTo, Thread},
33        room::message::{
34            MessageType, Relation, RoomMessageEventContent, RoomMessageEventContentWithoutRelation,
35            SyncRoomMessageEvent,
36        },
37        AnyMessageLikeEventContent, AnySyncMessageLikeEvent, AnySyncTimelineEvent,
38        BundledMessageLikeRelations, Mentions,
39    },
40    html::RemoveReplyFallback,
41    serde::Raw,
42    OwnedEventId, OwnedUserId, UserId,
43};
44use tracing::{debug, error, instrument, trace, warn};
45
46use super::TimelineItemContent;
47use crate::{
48    timeline::{
49        event_item::{EventTimelineItem, Profile, TimelineDetails},
50        traits::RoomDataProvider,
51        EncryptedMessage, Error as TimelineError, PollState, ReactionsByKeyBySender, Sticker,
52        TimelineItem,
53    },
54    DEFAULT_SANITIZER_MODE,
55};
56
57/// An `m.room.message` event or extensible event, including edits.
58#[derive(Clone)]
59pub struct Message {
60    pub(in crate::timeline) msgtype: MessageType,
61    pub(in crate::timeline) in_reply_to: Option<InReplyToDetails>,
62    /// Event ID of the thread root, if this is a threaded message.
63    pub(in crate::timeline) thread_root: Option<OwnedEventId>,
64    pub(in crate::timeline) edited: bool,
65    pub(in crate::timeline) mentions: Option<Mentions>,
66    pub(in crate::timeline) reactions: ReactionsByKeyBySender,
67}
68
69impl Message {
70    /// Construct a `Message` from a `m.room.message` event.
71    pub(in crate::timeline) fn from_event(
72        c: RoomMessageEventContent,
73        edit: Option<RoomMessageEventContentWithoutRelation>,
74        timeline_items: &Vector<Arc<TimelineItem>>,
75        reactions: ReactionsByKeyBySender,
76    ) -> Self {
77        let mut thread_root = None;
78        let in_reply_to = c.relates_to.and_then(|relation| match relation {
79            Relation::Reply { in_reply_to } => {
80                Some(InReplyToDetails::new(in_reply_to.event_id, timeline_items))
81            }
82            Relation::Thread(thread) => {
83                thread_root = Some(thread.event_id);
84                thread
85                    .in_reply_to
86                    .map(|in_reply_to| InReplyToDetails::new(in_reply_to.event_id, timeline_items))
87            }
88            _ => None,
89        });
90
91        let remove_reply_fallback =
92            if in_reply_to.is_some() { RemoveReplyFallback::Yes } else { RemoveReplyFallback::No };
93
94        let mut msgtype = c.msgtype;
95        msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
96
97        let mut ret = Self {
98            msgtype,
99            in_reply_to,
100            thread_root,
101            edited: false,
102            mentions: c.mentions,
103            reactions,
104        };
105
106        if let Some(edit) = edit {
107            ret.apply_edit(edit);
108        }
109
110        ret
111    }
112
113    /// Apply an edit to the current message.
114    pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) {
115        trace!("applying edit to a Message");
116        // Edit's content is never supposed to contain the reply fallback.
117        new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
118        self.msgtype = new_content.msgtype;
119        self.mentions = new_content.mentions;
120        self.edited = true;
121    }
122
123    /// Get the `msgtype`-specific data of this message.
124    pub fn msgtype(&self) -> &MessageType {
125        &self.msgtype
126    }
127
128    /// Get a reference to the message body.
129    ///
130    /// Shorthand for `.msgtype().body()`.
131    pub fn body(&self) -> &str {
132        self.msgtype.body()
133    }
134
135    /// Get the event this message is replying to, if any.
136    pub fn in_reply_to(&self) -> Option<&InReplyToDetails> {
137        self.in_reply_to.as_ref()
138    }
139
140    /// Whether this message is part of a thread.
141    pub fn is_threaded(&self) -> bool {
142        self.thread_root.is_some()
143    }
144
145    /// Get the [`OwnedEventId`] of the root event of a thread if it exists.
146    pub fn thread_root(&self) -> Option<&OwnedEventId> {
147        self.thread_root.as_ref()
148    }
149
150    /// Get the edit state of this message (has been edited: `true` /
151    /// `false`).
152    pub fn is_edited(&self) -> bool {
153        self.edited
154    }
155
156    /// Get the mentions of this message.
157    pub fn mentions(&self) -> Option<&Mentions> {
158        self.mentions.as_ref()
159    }
160
161    pub(in crate::timeline) fn to_content(&self) -> RoomMessageEventContent {
162        // Like the `impl From<Message> for RoomMessageEventContent` below, but
163        // takes &self and only copies what's needed.
164        let relates_to = make_relates_to(
165            self.thread_root.clone(),
166            self.in_reply_to.as_ref().map(|details| details.event_id.clone()),
167        );
168        assign!(RoomMessageEventContent::new(self.msgtype.clone()), { relates_to })
169    }
170
171    pub(in crate::timeline) fn with_in_reply_to(&self, in_reply_to: InReplyToDetails) -> Self {
172        Self { in_reply_to: Some(in_reply_to), ..self.clone() }
173    }
174}
175
176impl From<Message> for RoomMessageEventContent {
177    fn from(msg: Message) -> Self {
178        let relates_to =
179            make_relates_to(msg.thread_root, msg.in_reply_to.map(|details| details.event_id));
180        assign!(Self::new(msg.msgtype), { relates_to })
181    }
182}
183
184/// Extracts the raw json of the edit event part of bundled relations.
185///
186/// Note: while we had access to the deserialized event earlier, events are not
187/// serializable, by design of Ruma, so we can't extract a bundled related event
188/// and serialize it back to a raw JSON event.
189pub(crate) fn extract_bundled_edit_event_json(
190    raw: &Raw<AnySyncTimelineEvent>,
191) -> Option<Raw<AnySyncTimelineEvent>> {
192    // Follow the `unsigned`.`m.relations`.`m.replace` path.
193    let raw_unsigned: Raw<serde_json::Value> = raw.get_field("unsigned").ok()??;
194    let raw_relations: Raw<serde_json::Value> = raw_unsigned.get_field("m.relations").ok()??;
195    raw_relations.get_field::<Raw<AnySyncTimelineEvent>>("m.replace").ok()?
196}
197
198/// Extracts a replacement for a room message, if present in the bundled
199/// relations.
200pub(crate) fn extract_room_msg_edit_content(
201    relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
202) -> Option<RoomMessageEventContentWithoutRelation> {
203    match *relations.replace? {
204        AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
205            .content
206            .relates_to
207        {
208            Some(Relation::Replacement(re)) => {
209                trace!("found a bundled edit event in a room message");
210                Some(re.new_content)
211            }
212            _ => {
213                error!("got m.room.message event with an edit without a valid m.replace relation");
214                None
215            }
216        },
217
218        AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
219
220        _ => {
221            error!("got m.room.message event with an edit of a different event type");
222            None
223        }
224    }
225}
226
227/// Extracts a replacement for a room message, if present in the bundled
228/// relations.
229pub(crate) fn extract_poll_edit_content(
230    relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
231) -> Option<NewUnstablePollStartEventContentWithoutRelation> {
232    match *relations.replace? {
233        AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => {
234            match ev.content {
235                UnstablePollStartEventContent::Replacement(re) => {
236                    trace!("found a bundled edit event in a poll");
237                    Some(re.relates_to.new_content)
238                }
239                _ => {
240                    error!("got new poll start event in a bundled edit");
241                    None
242                }
243            }
244        }
245
246        AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None,
247
248        _ => {
249            error!("got poll edit event with an edit of a different event type");
250            None
251        }
252    }
253}
254
255/// Turn a pair of thread root ID and in-reply-to ID as stored in [`Message`]
256/// back into a [`Relation`].
257///
258/// This doesn't properly handle the distinction between reply relations in
259/// threads that just exist as fallbacks, and "real" thread + reply relations.
260/// For our use, this is okay though.
261fn make_relates_to(
262    thread_root: Option<OwnedEventId>,
263    in_reply_to: Option<OwnedEventId>,
264) -> Option<Relation<RoomMessageEventContentWithoutRelation>> {
265    match (thread_root, in_reply_to) {
266        (Some(thread_root), Some(in_reply_to)) => {
267            Some(Relation::Thread(Thread::plain(thread_root, in_reply_to)))
268        }
269        (Some(thread_root), None) => Some(Relation::Thread(Thread::without_fallback(thread_root))),
270        (None, Some(in_reply_to)) => {
271            Some(Relation::Reply { in_reply_to: InReplyTo::new(in_reply_to) })
272        }
273        (None, None) => None,
274    }
275}
276
277#[cfg(not(tarpaulin_include))]
278impl fmt::Debug for Message {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        let Self { msgtype: _, in_reply_to, thread_root, edited, reactions: _, mentions: _ } = self;
281        // since timeline items are logged, don't include all fields here so
282        // people don't leak personal data in bug reports
283        f.debug_struct("Message")
284            .field("in_reply_to", in_reply_to)
285            .field("thread_root", thread_root)
286            .field("edited", edited)
287            .finish_non_exhaustive()
288    }
289}
290
291/// Details about an event being replied to.
292#[derive(Clone, Debug)]
293pub struct InReplyToDetails {
294    /// The ID of the event.
295    pub event_id: OwnedEventId,
296
297    /// The details of the event.
298    ///
299    /// Use [`Timeline::fetch_details_for_event`] to fetch the data if it is
300    /// unavailable.
301    ///
302    /// [`Timeline::fetch_details_for_event`]: crate::Timeline::fetch_details_for_event
303    pub event: TimelineDetails<Box<RepliedToEvent>>,
304}
305
306impl InReplyToDetails {
307    pub fn new(
308        event_id: OwnedEventId,
309        timeline_items: &Vector<Arc<TimelineItem>>,
310    ) -> InReplyToDetails {
311        let event = timeline_items
312            .iter()
313            .filter_map(|it| it.as_event())
314            .find(|it| it.event_id() == Some(&*event_id))
315            .map(|item| Box::new(RepliedToEvent::from_timeline_item(item)));
316
317        InReplyToDetails { event_id, event: TimelineDetails::from_initial_value(event) }
318    }
319}
320
321/// An event that is replied to.
322#[derive(Clone, Debug)]
323pub struct RepliedToEvent {
324    pub(in crate::timeline) content: TimelineItemContent,
325    pub(in crate::timeline) sender: OwnedUserId,
326    pub(in crate::timeline) sender_profile: TimelineDetails<Profile>,
327}
328
329impl RepliedToEvent {
330    /// Get the message of this event.
331    pub fn content(&self) -> &TimelineItemContent {
332        &self.content
333    }
334
335    /// Get the sender of this event.
336    pub fn sender(&self) -> &UserId {
337        &self.sender
338    }
339
340    /// Get the profile of the sender.
341    pub fn sender_profile(&self) -> &TimelineDetails<Profile> {
342        &self.sender_profile
343    }
344
345    pub fn from_timeline_item(timeline_item: &EventTimelineItem) -> Self {
346        Self {
347            content: timeline_item.content.clone(),
348            sender: timeline_item.sender.clone(),
349            sender_profile: timeline_item.sender_profile.clone(),
350        }
351    }
352
353    /// Try to create a `RepliedToEvent` from a `TimelineEvent` by providing the
354    /// room.
355    pub async fn try_from_timeline_event_for_room(
356        timeline_event: TimelineEvent,
357        room_data_provider: &Room,
358    ) -> Result<Self, TimelineError> {
359        Self::try_from_timeline_event(timeline_event, room_data_provider).await
360    }
361
362    #[instrument(skip_all)]
363    pub(in crate::timeline) async fn try_from_timeline_event<P: RoomDataProvider>(
364        timeline_event: TimelineEvent,
365        room_data_provider: &P,
366    ) -> Result<Self, TimelineError> {
367        let event = match timeline_event.raw().deserialize() {
368            Ok(AnySyncTimelineEvent::MessageLike(event)) => event,
369            Ok(_) => {
370                warn!("can't get details, event isn't a message-like event");
371                return Err(TimelineError::UnsupportedEvent);
372            }
373            Err(err) => {
374                warn!("can't get details, event couldn't be deserialized: {err}");
375                return Err(TimelineError::UnsupportedEvent);
376            }
377        };
378
379        debug!(event_type = %event.event_type(), "got deserialized event");
380
381        let content = match event.original_content() {
382            Some(content) => match content {
383                AnyMessageLikeEventContent::RoomMessage(c) => {
384                    // Assume we're not interested in reactions in this context: this is
385                    // information for an embedded (replied-to) event, that will usually not
386                    // include detailed information like reactions.
387                    let reactions = ReactionsByKeyBySender::default();
388
389                    TimelineItemContent::Message(Message::from_event(
390                        c,
391                        extract_room_msg_edit_content(event.relations()),
392                        &vector![],
393                        reactions,
394                    ))
395                }
396
397                AnyMessageLikeEventContent::Sticker(content) => {
398                    // Assume we're not interested in reactions in this context. (See above an
399                    // explanation as to why that's the case.)
400                    let reactions = ReactionsByKeyBySender::default();
401                    TimelineItemContent::Sticker(Sticker { content, reactions })
402                }
403
404                AnyMessageLikeEventContent::RoomEncrypted(content) => {
405                    let utd_cause = match &timeline_event.kind {
406                        TimelineEventKind::UnableToDecrypt { utd_info, .. } => UtdCause::determine(
407                            timeline_event.raw(),
408                            room_data_provider.crypto_context_info().await,
409                            utd_info,
410                        ),
411                        _ => UtdCause::Unknown,
412                    };
413
414                    TimelineItemContent::UnableToDecrypt(EncryptedMessage::from_content(
415                        content, utd_cause,
416                    ))
417                }
418
419                AnyMessageLikeEventContent::UnstablePollStart(
420                    UnstablePollStartEventContent::New(content),
421                ) => {
422                    // Assume we're not interested in reactions in this context. (See above an
423                    // explanation as to why that's the case.)
424                    let reactions = ReactionsByKeyBySender::default();
425                    // TODO: could we provide the bundled edit here?
426                    let poll_state = PollState::new(content, None, reactions);
427                    TimelineItemContent::Poll(poll_state)
428                }
429
430                _ => {
431                    warn!("unsupported event type");
432                    return Err(TimelineError::UnsupportedEvent);
433                }
434            },
435
436            None => {
437                // Redacted message.
438                TimelineItemContent::RedactedMessage
439            }
440        };
441
442        let sender = event.sender().to_owned();
443        let sender_profile = TimelineDetails::from_initial_value(
444            room_data_provider.profile_from_user_id(&sender).await,
445        );
446
447        Ok(Self { content, sender, sender_profile })
448    }
449}