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