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