Skip to main content

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;
18
19use ruma::{
20    OwnedEventId,
21    events::{
22        AnySyncMessageLikeEvent, AnySyncTimelineEvent, BundledMessageLikeRelations, Mentions,
23        poll::unstable_start::{
24            NewUnstablePollStartEventContentWithoutRelation, SyncUnstablePollStartEvent,
25            UnstablePollStartEventContent,
26        },
27        room::message::{
28            MessageType, Relation, RoomMessageEventContentWithoutRelation, SyncRoomMessageEvent,
29        },
30    },
31    html::RemoveReplyFallback,
32    serde::Raw,
33};
34use tracing::{error, trace};
35
36use crate::DEFAULT_SANITIZER_MODE;
37
38/// An `m.room.message` event or extensible event, including edits.
39#[derive(Clone)]
40pub struct Message {
41    pub(in crate::timeline) msgtype: MessageType,
42    pub(in crate::timeline) edited: bool,
43    pub(in crate::timeline) mentions: Option<Mentions>,
44}
45
46impl Message {
47    /// Construct a `Message` from a `m.room.message` event.
48    pub(in crate::timeline) fn from_event(
49        mut msgtype: MessageType,
50        mentions: Option<Mentions>,
51        edit: Option<RoomMessageEventContentWithoutRelation>,
52        remove_reply_fallback: RemoveReplyFallback,
53    ) -> Self {
54        msgtype.sanitize(DEFAULT_SANITIZER_MODE, remove_reply_fallback);
55
56        let mut ret = Self { msgtype, edited: false, mentions };
57
58        if let Some(edit) = edit {
59            ret.apply_edit(edit);
60        }
61
62        ret
63    }
64
65    /// Apply an edit to the current message.
66    pub(crate) fn apply_edit(&mut self, mut new_content: RoomMessageEventContentWithoutRelation) {
67        trace!("applying edit to a Message");
68        // Edit's content is never supposed to contain the reply fallback.
69        new_content.msgtype.sanitize(DEFAULT_SANITIZER_MODE, RemoveReplyFallback::No);
70        self.msgtype = new_content.msgtype;
71        self.mentions = new_content.mentions;
72        self.edited = true;
73    }
74
75    /// Get the `msgtype`-specific data of this message.
76    pub fn msgtype(&self) -> &MessageType {
77        &self.msgtype
78    }
79
80    /// Get a reference to the message body.
81    ///
82    /// Shorthand for `.msgtype().body()`.
83    pub fn body(&self) -> &str {
84        self.msgtype.body()
85    }
86
87    /// Get the edit state of this message (has been edited: `true` / `false`).
88    pub fn is_edited(&self) -> bool {
89        self.edited
90    }
91
92    /// Get the mentions of this message.
93    pub fn mentions(&self) -> Option<&Mentions> {
94        self.mentions.as_ref()
95    }
96}
97
98/// Extracts the raw json of the edit event part of bundled relations.
99///
100/// Note: while we had access to the deserialized event earlier, events are not
101/// serializable, by design of Ruma, so we can't extract a bundled related event
102/// and serialize it back to a raw JSON event.
103pub(crate) fn extract_bundled_edit_event_json(
104    raw: &Raw<AnySyncTimelineEvent>,
105) -> Option<Raw<AnySyncTimelineEvent>> {
106    // Follow the `unsigned`.`m.relations`.`m.replace` path.
107    let raw_unsigned: Raw<serde_json::Value> = raw.get_field("unsigned").ok()??;
108    let raw_relations: Raw<serde_json::Value> = raw_unsigned.get_field("m.relations").ok()??;
109    raw_relations.get_field::<Raw<AnySyncTimelineEvent>>("m.replace").ok()?
110}
111
112/// Extracts a replacement for a room message, if present in the bundled
113/// relations , along with the event ID of the replacement event.
114pub(crate) fn extract_room_msg_edit_content(
115    relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
116) -> Option<(OwnedEventId, RoomMessageEventContentWithoutRelation)> {
117    match *relations.replace? {
118        AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Original(ev)) => match ev
119            .content
120            .relates_to
121        {
122            Some(Relation::Replacement(re)) => {
123                trace!("found a bundled edit event in a room message");
124                Some((ev.event_id, re.new_content))
125            }
126            _ => {
127                error!("got m.room.message event with an edit without a valid m.replace relation");
128                None
129            }
130        },
131
132        AnySyncMessageLikeEvent::RoomMessage(SyncRoomMessageEvent::Redacted(_)) => None,
133
134        _ => {
135            error!("got m.room.message event with an edit of a different event type");
136            None
137        }
138    }
139}
140
141/// Extracts a replacement for a room message, if present in the bundled
142/// relations, along with the event ID of the replacement event.
143pub(crate) fn extract_poll_edit_content(
144    relations: BundledMessageLikeRelations<AnySyncMessageLikeEvent>,
145) -> Option<(OwnedEventId, NewUnstablePollStartEventContentWithoutRelation)> {
146    match *relations.replace? {
147        AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Original(ev)) => {
148            match ev.content {
149                UnstablePollStartEventContent::Replacement(re) => {
150                    trace!("found a bundled edit event in a poll");
151                    Some((ev.event_id, re.relates_to.new_content))
152                }
153                _ => {
154                    error!("got new poll start event in a bundled edit");
155                    None
156                }
157            }
158        }
159
160        AnySyncMessageLikeEvent::UnstablePollStart(SyncUnstablePollStartEvent::Redacted(_)) => None,
161
162        _ => {
163            error!("got poll edit event with an edit of a different event type");
164            None
165        }
166    }
167}
168
169#[cfg(not(tarpaulin_include))]
170impl fmt::Debug for Message {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        let Self { msgtype: _, edited, mentions: _ } = self;
173        // since timeline items are logged, don't include all fields here so
174        // people don't leak personal data in bug reports
175        f.debug_struct("Message").field("edited", edited).finish_non_exhaustive()
176    }
177}