ruma_events/room/
message.rs

1//! Types for the [`m.room.message`] event.
2//!
3//! [`m.room.message`]: https://spec.matrix.org/latest/client-server-api/#mroommessage
4
5use std::borrow::Cow;
6
7use ruma_common::{
8    serde::{JsonObject, Raw, StringEnum},
9    OwnedEventId, RoomId,
10};
11#[cfg(feature = "html")]
12use ruma_html::{sanitize_html, HtmlSanitizerMode, RemoveReplyFallback};
13use ruma_macros::EventContent;
14use serde::{de::DeserializeOwned, Deserialize, Serialize};
15use serde_json::Value as JsonValue;
16use tracing::warn;
17
18use self::reply::OriginalEventData;
19#[cfg(feature = "html")]
20use self::sanitize::remove_plain_reply_fallback;
21use crate::{
22    relation::{InReplyTo, Replacement, Thread},
23    AnySyncTimelineEvent, Mentions, PrivOwnedStr,
24};
25
26mod audio;
27mod content_serde;
28mod emote;
29mod file;
30#[cfg(feature = "unstable-msc4274")]
31mod gallery;
32mod image;
33mod key_verification_request;
34mod location;
35mod media_caption;
36mod notice;
37mod relation;
38pub(crate) mod relation_serde;
39mod reply;
40pub mod sanitize;
41mod server_notice;
42mod text;
43#[cfg(feature = "unstable-msc4095")]
44mod url_preview;
45mod video;
46mod without_relation;
47
48#[cfg(feature = "unstable-msc3245-v1-compat")]
49pub use self::audio::{
50    UnstableAmplitude, UnstableAudioDetailsContentBlock, UnstableVoiceContentBlock,
51};
52#[cfg(feature = "unstable-msc4274")]
53pub use self::gallery::{GalleryItemType, GalleryMessageEventContent};
54#[cfg(feature = "unstable-msc4095")]
55pub use self::url_preview::{PreviewImage, PreviewImageSource, UrlPreview};
56pub use self::{
57    audio::{AudioInfo, AudioMessageEventContent},
58    emote::EmoteMessageEventContent,
59    file::{FileInfo, FileMessageEventContent},
60    image::ImageMessageEventContent,
61    key_verification_request::KeyVerificationRequestEventContent,
62    location::{LocationInfo, LocationMessageEventContent},
63    notice::NoticeMessageEventContent,
64    relation::{Relation, RelationWithoutReplacement},
65    relation_serde::deserialize_relation,
66    server_notice::{LimitType, ServerNoticeMessageEventContent, ServerNoticeType},
67    text::TextMessageEventContent,
68    video::{VideoInfo, VideoMessageEventContent},
69    without_relation::RoomMessageEventContentWithoutRelation,
70};
71
72/// The content of an `m.room.message` event.
73///
74/// This event is used when sending messages in a room.
75///
76/// Messages are not limited to be text.
77#[derive(Clone, Debug, Serialize, EventContent)]
78#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
79#[ruma_event(type = "m.room.message", kind = MessageLike)]
80pub struct RoomMessageEventContent {
81    /// A key which identifies the type of message being sent.
82    ///
83    /// This also holds the specific content of each message.
84    #[serde(flatten)]
85    pub msgtype: MessageType,
86
87    /// Information about [related messages].
88    ///
89    /// [related messages]: https://spec.matrix.org/latest/client-server-api/#forming-relationships-between-events
90    #[serde(flatten, skip_serializing_if = "Option::is_none")]
91    pub relates_to: Option<Relation<RoomMessageEventContentWithoutRelation>>,
92
93    /// The [mentions] of this event.
94    ///
95    /// This should always be set to avoid triggering the legacy mention push rules. It is
96    /// recommended to use [`Self::set_mentions()`] to set this field, that will take care of
97    /// populating the fields correctly if this is a replacement.
98    ///
99    /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
100    #[serde(rename = "m.mentions", skip_serializing_if = "Option::is_none")]
101    pub mentions: Option<Mentions>,
102}
103
104impl RoomMessageEventContent {
105    /// Create a `RoomMessageEventContent` with the given `MessageType`.
106    pub fn new(msgtype: MessageType) -> Self {
107        Self { msgtype, relates_to: None, mentions: None }
108    }
109
110    /// A constructor to create a plain text message.
111    pub fn text_plain(body: impl Into<String>) -> Self {
112        Self::new(MessageType::text_plain(body))
113    }
114
115    /// A constructor to create an html message.
116    pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
117        Self::new(MessageType::text_html(body, html_body))
118    }
119
120    /// A constructor to create a markdown message.
121    #[cfg(feature = "markdown")]
122    pub fn text_markdown(body: impl AsRef<str> + Into<String>) -> Self {
123        Self::new(MessageType::text_markdown(body))
124    }
125
126    /// A constructor to create a plain text notice.
127    pub fn notice_plain(body: impl Into<String>) -> Self {
128        Self::new(MessageType::notice_plain(body))
129    }
130
131    /// A constructor to create an html notice.
132    pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
133        Self::new(MessageType::notice_html(body, html_body))
134    }
135
136    /// A constructor to create a markdown notice.
137    #[cfg(feature = "markdown")]
138    pub fn notice_markdown(body: impl AsRef<str> + Into<String>) -> Self {
139        Self::new(MessageType::notice_markdown(body))
140    }
141
142    /// A constructor to create a plain text emote.
143    pub fn emote_plain(body: impl Into<String>) -> Self {
144        Self::new(MessageType::emote_plain(body))
145    }
146
147    /// A constructor to create an html emote.
148    pub fn emote_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
149        Self::new(MessageType::emote_html(body, html_body))
150    }
151
152    /// A constructor to create a markdown emote.
153    #[cfg(feature = "markdown")]
154    pub fn emote_markdown(body: impl AsRef<str> + Into<String>) -> Self {
155        Self::new(MessageType::emote_markdown(body))
156    }
157
158    /// Turns `self` into a reply to the given message.
159    ///
160    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
161    /// quoted version of `original_message`. Also sets the `in_reply_to` field inside `relates_to`,
162    /// and optionally the `rel_type` to `m.thread` if the `original_message is in a thread and
163    /// thread forwarding is enabled.
164    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
165    ///
166    /// # Panics
167    ///
168    /// Panics if `self` has a `formatted_body` with a format other than HTML.
169    #[track_caller]
170    pub fn make_reply_to(
171        self,
172        original_message: &OriginalRoomMessageEvent,
173        forward_thread: ForwardThread,
174        add_mentions: AddMentions,
175    ) -> Self {
176        self.without_relation().make_reply_to(original_message, forward_thread, add_mentions)
177    }
178
179    /// Turns `self` into a reply to the given raw event.
180    ///
181    /// Takes the `body` / `formatted_body` (if any) in `self` for the main text and prepends a
182    /// quoted version of the `body` of `original_event` (if any). Also sets the `in_reply_to` field
183    /// inside `relates_to`, and optionally the `rel_type` to `m.thread` if the
184    /// `original_message is in a thread and thread forwarding is enabled.
185    ///
186    /// It is recommended to use [`Self::make_reply_to()`] for replies to `m.room.message` events,
187    /// as the generated fallback is better for some `msgtype`s.
188    ///
189    /// Note that except for the panic below, this is infallible. Which means that if a field is
190    /// missing when deserializing the data, the changes that require it will not be applied. It
191    /// will still at least apply the `m.in_reply_to` relation to this content.
192    ///
193    /// # Panics
194    ///
195    /// Panics if `self` has a `formatted_body` with a format other than HTML.
196    #[track_caller]
197    pub fn make_reply_to_raw(
198        self,
199        original_event: &Raw<AnySyncTimelineEvent>,
200        original_event_id: OwnedEventId,
201        room_id: &RoomId,
202        forward_thread: ForwardThread,
203        add_mentions: AddMentions,
204    ) -> Self {
205        self.without_relation().make_reply_to_raw(
206            original_event,
207            original_event_id,
208            room_id,
209            forward_thread,
210            add_mentions,
211        )
212    }
213
214    /// Turns `self` into a new message for a thread, that is optionally a reply.
215    ///
216    /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, this message will be
217    /// in the same thread. If it doesn't, a new thread with `previous_message` as the root is
218    /// created.
219    ///
220    /// If this is a reply within the thread, takes the `body` / `formatted_body` (if any) in `self`
221    /// for the main text and prepends a quoted version of `previous_message`. Also sets the
222    /// `in_reply_to` field inside `relates_to`.
223    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
224    ///
225    /// # Panics
226    ///
227    /// Panics if this is a reply within the thread and `self` has a `formatted_body` with a format
228    /// other than HTML.
229    pub fn make_for_thread(
230        self,
231        previous_message: &OriginalRoomMessageEvent,
232        is_reply: ReplyWithinThread,
233        add_mentions: AddMentions,
234    ) -> Self {
235        self.without_relation().make_for_thread(previous_message, is_reply, add_mentions)
236    }
237
238    /// Turns `self` into a [replacement] (or edit) for a given message.
239    ///
240    /// The first argument after `self` can be `&OriginalRoomMessageEvent` or
241    /// `&OriginalSyncRoomMessageEvent` if you don't want to create `ReplacementMetadata` separately
242    /// before calling this function.
243    ///
244    /// This takes the content and sets it in `m.new_content`, and modifies the `content` to include
245    /// a fallback.
246    ///
247    /// If the message that is replaced is a reply to another message, the latter should also be
248    /// provided to be able to generate a rich reply fallback that takes the `body` /
249    /// `formatted_body` (if any) in `self` for the main text and prepends a quoted version of
250    /// `original_message`.
251    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/rich_reply.md"))]
252    ///
253    /// If this message contains [`Mentions`], they are copied into `m.new_content` to keep the same
254    /// mentions, but the ones in `content` are filtered with the ones in the
255    /// [`ReplacementMetadata`] so only new mentions will trigger a notification.
256    ///
257    /// # Panics
258    ///
259    /// Panics if `self` has a `formatted_body` with a format other than HTML.
260    ///
261    /// [replacement]: https://spec.matrix.org/latest/client-server-api/#event-replacements
262    #[track_caller]
263    pub fn make_replacement(
264        self,
265        metadata: impl Into<ReplacementMetadata>,
266        replied_to_message: Option<&OriginalRoomMessageEvent>,
267    ) -> Self {
268        self.without_relation().make_replacement(metadata, replied_to_message)
269    }
270
271    /// Set the [mentions] of this event.
272    ///
273    /// If this event is a replacement, it will update the mentions both in the `content` and the
274    /// `m.new_content` so only new mentions will trigger a notification. As such, this needs to be
275    /// called after [`Self::make_replacement()`].
276    ///
277    /// It is not recommended to call this method after one that sets mentions automatically, like
278    /// [`Self::make_reply_to()`] as these will be overwritten. [`Self::add_mentions()`] should be
279    /// used instead.
280    ///
281    /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
282    #[deprecated = "Call add_mentions before adding the relation instead."]
283    pub fn set_mentions(mut self, mentions: Mentions) -> Self {
284        if let Some(Relation::Replacement(replacement)) = &mut self.relates_to {
285            let old_mentions = &replacement.new_content.mentions;
286
287            let new_mentions = if let Some(old_mentions) = old_mentions {
288                let mut new_mentions = Mentions::new();
289
290                new_mentions.user_ids = mentions
291                    .user_ids
292                    .iter()
293                    .filter(|u| !old_mentions.user_ids.contains(*u))
294                    .cloned()
295                    .collect();
296
297                new_mentions.room = mentions.room && !old_mentions.room;
298
299                new_mentions
300            } else {
301                mentions.clone()
302            };
303
304            replacement.new_content.mentions = Some(mentions);
305            self.mentions = Some(new_mentions);
306        } else {
307            self.mentions = Some(mentions);
308        }
309
310        self
311    }
312
313    /// Add the given [mentions] to this event.
314    ///
315    /// If no [`Mentions`] was set on this events, this sets it. Otherwise, this updates the current
316    /// mentions by extending the previous `user_ids` with the new ones, and applies a logical OR to
317    /// the values of `room`.
318    ///
319    /// This should be called before methods that add a relation, like [`Self::make_reply_to()`] and
320    /// [`Self::make_replacement()`], for the mentions to be correctly set.
321    ///
322    /// [mentions]: https://spec.matrix.org/latest/client-server-api/#user-and-room-mentions
323    pub fn add_mentions(mut self, mentions: Mentions) -> Self {
324        self.mentions.get_or_insert_with(Mentions::new).add(mentions);
325        self
326    }
327
328    /// Returns a reference to the `msgtype` string.
329    ///
330    /// If you want to access the message type-specific data rather than the message type itself,
331    /// use the `msgtype` *field*, not this method.
332    pub fn msgtype(&self) -> &str {
333        self.msgtype.msgtype()
334    }
335
336    /// Return a reference to the message body.
337    pub fn body(&self) -> &str {
338        self.msgtype.body()
339    }
340
341    /// Apply the given new content from a [`Replacement`] to this message.
342    pub fn apply_replacement(&mut self, new_content: RoomMessageEventContentWithoutRelation) {
343        let RoomMessageEventContentWithoutRelation { msgtype, mentions } = new_content;
344        self.msgtype = msgtype;
345        self.mentions = mentions;
346    }
347
348    /// Sanitize this message.
349    ///
350    /// If this message contains HTML, this removes the [tags and attributes] that are not listed in
351    /// the Matrix specification.
352    ///
353    /// It can also optionally remove the [rich reply] fallback from the plain text and HTML
354    /// message.
355    ///
356    /// This method is only effective on text, notice and emote messages.
357    ///
358    /// [tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
359    /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
360    #[cfg(feature = "html")]
361    pub fn sanitize(
362        &mut self,
363        mode: HtmlSanitizerMode,
364        remove_reply_fallback: RemoveReplyFallback,
365    ) {
366        let remove_reply_fallback = if matches!(self.relates_to, Some(Relation::Reply { .. })) {
367            remove_reply_fallback
368        } else {
369            RemoveReplyFallback::No
370        };
371
372        self.msgtype.sanitize(mode, remove_reply_fallback);
373    }
374
375    fn without_relation(self) -> RoomMessageEventContentWithoutRelation {
376        if self.relates_to.is_some() {
377            warn!("Overwriting existing relates_to value");
378        }
379
380        self.into()
381    }
382}
383
384/// Whether or not to forward a [`Relation::Thread`] when sending a reply.
385#[derive(Clone, Copy, Debug, PartialEq, Eq)]
386#[allow(clippy::exhaustive_enums)]
387pub enum ForwardThread {
388    /// The thread relation in the original message is forwarded if it exists.
389    ///
390    /// This should be set if your client doesn't render threads (see the [info
391    /// box for clients which are acutely aware of threads]).
392    ///
393    /// [info box for clients which are acutely aware of threads]: https://spec.matrix.org/latest/client-server-api/#fallback-for-unthreaded-clients
394    Yes,
395
396    /// Create a reply in the main conversation even if the original message is in a thread.
397    ///
398    /// This should be used if you client supports threads and you explicitly want that behavior.
399    No,
400}
401
402/// Whether or not to add intentional [`Mentions`] when sending a reply.
403#[derive(Clone, Copy, Debug, PartialEq, Eq)]
404#[allow(clippy::exhaustive_enums)]
405pub enum AddMentions {
406    /// Add automatic intentional mentions to the reply.
407    ///
408    /// Set this if your client supports intentional mentions.
409    ///
410    /// The sender of the original event will be added to the mentions of this message.
411    Yes,
412
413    /// Do not add intentional mentions to the reply.
414    ///
415    /// Set this if your client does not support intentional mentions.
416    No,
417}
418
419/// Whether or not the message is a reply inside a thread.
420#[derive(Clone, Copy, Debug, PartialEq, Eq)]
421#[allow(clippy::exhaustive_enums)]
422pub enum ReplyWithinThread {
423    /// This is a reply.
424    ///
425    /// Create a [reply within the thread].
426    ///
427    /// [reply within the thread]: https://spec.matrix.org/latest/client-server-api/#replies-within-threads
428    Yes,
429
430    /// This is not a reply.
431    ///
432    /// Create a regular message in the thread, with a [fallback for unthreaded clients].
433    ///
434    /// [fallback for unthreaded clients]: https://spec.matrix.org/latest/client-server-api/#fallback-for-unthreaded-clients
435    No,
436}
437
438/// The content that is specific to each message type variant.
439#[derive(Clone, Debug, Serialize)]
440#[serde(untagged)]
441#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
442pub enum MessageType {
443    /// An audio message.
444    Audio(AudioMessageEventContent),
445
446    /// An emote message.
447    Emote(EmoteMessageEventContent),
448
449    /// A file message.
450    File(FileMessageEventContent),
451
452    /// A media gallery message.
453    #[cfg(feature = "unstable-msc4274")]
454    Gallery(GalleryMessageEventContent),
455
456    /// An image message.
457    Image(ImageMessageEventContent),
458
459    /// A location message.
460    Location(LocationMessageEventContent),
461
462    /// A notice message.
463    Notice(NoticeMessageEventContent),
464
465    /// A server notice message.
466    ServerNotice(ServerNoticeMessageEventContent),
467
468    /// A text message.
469    Text(TextMessageEventContent),
470
471    /// A video message.
472    Video(VideoMessageEventContent),
473
474    /// A request to initiate a key verification.
475    VerificationRequest(KeyVerificationRequestEventContent),
476
477    /// A custom message.
478    #[doc(hidden)]
479    _Custom(CustomEventContent),
480}
481
482impl MessageType {
483    /// Creates a new `MessageType`.
484    ///
485    /// The `msgtype` and `body` are required fields as defined by [the `m.room.message` spec](https://spec.matrix.org/latest/client-server-api/#mroommessage).
486    /// Additionally it's possible to add arbitrary key/value pairs to the event content for custom
487    /// events through the `data` map.
488    ///
489    /// Prefer to use the public variants of `MessageType` where possible; this constructor is meant
490    /// be used for unsupported message types only and does not allow setting arbitrary data for
491    /// supported ones.
492    ///
493    /// # Errors
494    ///
495    /// Returns an error if the `msgtype` is known and serialization of `data` to the corresponding
496    /// `MessageType` variant fails.
497    pub fn new(msgtype: &str, body: String, data: JsonObject) -> serde_json::Result<Self> {
498        fn deserialize_variant<T: DeserializeOwned>(
499            body: String,
500            mut obj: JsonObject,
501        ) -> serde_json::Result<T> {
502            obj.insert("body".into(), body.into());
503            serde_json::from_value(JsonValue::Object(obj))
504        }
505
506        Ok(match msgtype {
507            "m.audio" => Self::Audio(deserialize_variant(body, data)?),
508            "m.emote" => Self::Emote(deserialize_variant(body, data)?),
509            "m.file" => Self::File(deserialize_variant(body, data)?),
510            #[cfg(feature = "unstable-msc4274")]
511            "dm.filament.gallery" => Self::Gallery(deserialize_variant(body, data)?),
512            "m.image" => Self::Image(deserialize_variant(body, data)?),
513            "m.location" => Self::Location(deserialize_variant(body, data)?),
514            "m.notice" => Self::Notice(deserialize_variant(body, data)?),
515            "m.server_notice" => Self::ServerNotice(deserialize_variant(body, data)?),
516            "m.text" => Self::Text(deserialize_variant(body, data)?),
517            "m.video" => Self::Video(deserialize_variant(body, data)?),
518            "m.key.verification.request" => {
519                Self::VerificationRequest(deserialize_variant(body, data)?)
520            }
521            _ => Self::_Custom(CustomEventContent { msgtype: msgtype.to_owned(), body, data }),
522        })
523    }
524
525    /// A constructor to create a plain text message.
526    pub fn text_plain(body: impl Into<String>) -> Self {
527        Self::Text(TextMessageEventContent::plain(body))
528    }
529
530    /// A constructor to create an html message.
531    pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
532        Self::Text(TextMessageEventContent::html(body, html_body))
533    }
534
535    /// A constructor to create a markdown message.
536    #[cfg(feature = "markdown")]
537    pub fn text_markdown(body: impl AsRef<str> + Into<String>) -> Self {
538        Self::Text(TextMessageEventContent::markdown(body))
539    }
540
541    /// A constructor to create a plain text notice.
542    pub fn notice_plain(body: impl Into<String>) -> Self {
543        Self::Notice(NoticeMessageEventContent::plain(body))
544    }
545
546    /// A constructor to create an html notice.
547    pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
548        Self::Notice(NoticeMessageEventContent::html(body, html_body))
549    }
550
551    /// A constructor to create a markdown notice.
552    #[cfg(feature = "markdown")]
553    pub fn notice_markdown(body: impl AsRef<str> + Into<String>) -> Self {
554        Self::Notice(NoticeMessageEventContent::markdown(body))
555    }
556
557    /// A constructor to create a plain text emote.
558    pub fn emote_plain(body: impl Into<String>) -> Self {
559        Self::Emote(EmoteMessageEventContent::plain(body))
560    }
561
562    /// A constructor to create an html emote.
563    pub fn emote_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
564        Self::Emote(EmoteMessageEventContent::html(body, html_body))
565    }
566
567    /// A constructor to create a markdown emote.
568    #[cfg(feature = "markdown")]
569    pub fn emote_markdown(body: impl AsRef<str> + Into<String>) -> Self {
570        Self::Emote(EmoteMessageEventContent::markdown(body))
571    }
572
573    /// Returns a reference to the `msgtype` string.
574    pub fn msgtype(&self) -> &str {
575        match self {
576            Self::Audio(_) => "m.audio",
577            Self::Emote(_) => "m.emote",
578            Self::File(_) => "m.file",
579            #[cfg(feature = "unstable-msc4274")]
580            Self::Gallery(_) => "dm.filament.gallery",
581            Self::Image(_) => "m.image",
582            Self::Location(_) => "m.location",
583            Self::Notice(_) => "m.notice",
584            Self::ServerNotice(_) => "m.server_notice",
585            Self::Text(_) => "m.text",
586            Self::Video(_) => "m.video",
587            Self::VerificationRequest(_) => "m.key.verification.request",
588            Self::_Custom(c) => &c.msgtype,
589        }
590    }
591
592    /// Return a reference to the message body.
593    pub fn body(&self) -> &str {
594        match self {
595            MessageType::Audio(m) => &m.body,
596            MessageType::Emote(m) => &m.body,
597            MessageType::File(m) => &m.body,
598            #[cfg(feature = "unstable-msc4274")]
599            MessageType::Gallery(m) => &m.body,
600            MessageType::Image(m) => &m.body,
601            MessageType::Location(m) => &m.body,
602            MessageType::Notice(m) => &m.body,
603            MessageType::ServerNotice(m) => &m.body,
604            MessageType::Text(m) => &m.body,
605            MessageType::Video(m) => &m.body,
606            MessageType::VerificationRequest(m) => &m.body,
607            MessageType::_Custom(m) => &m.body,
608        }
609    }
610
611    /// Returns the associated data.
612    ///
613    /// The returned JSON object won't contain the `msgtype` and `body` fields, use
614    /// [`.msgtype()`][Self::msgtype] / [`.body()`](Self::body) to access those.
615    ///
616    /// Prefer to use the public variants of `MessageType` where possible; this method is meant to
617    /// be used for custom message types only.
618    pub fn data(&self) -> Cow<'_, JsonObject> {
619        fn serialize<T: Serialize>(obj: &T) -> JsonObject {
620            match serde_json::to_value(obj).expect("message type serialization to succeed") {
621                JsonValue::Object(mut obj) => {
622                    obj.remove("body");
623                    obj
624                }
625                _ => panic!("all message types must serialize to objects"),
626            }
627        }
628
629        match self {
630            Self::Audio(d) => Cow::Owned(serialize(d)),
631            Self::Emote(d) => Cow::Owned(serialize(d)),
632            Self::File(d) => Cow::Owned(serialize(d)),
633            #[cfg(feature = "unstable-msc4274")]
634            Self::Gallery(d) => Cow::Owned(serialize(d)),
635            Self::Image(d) => Cow::Owned(serialize(d)),
636            Self::Location(d) => Cow::Owned(serialize(d)),
637            Self::Notice(d) => Cow::Owned(serialize(d)),
638            Self::ServerNotice(d) => Cow::Owned(serialize(d)),
639            Self::Text(d) => Cow::Owned(serialize(d)),
640            Self::Video(d) => Cow::Owned(serialize(d)),
641            Self::VerificationRequest(d) => Cow::Owned(serialize(d)),
642            Self::_Custom(c) => Cow::Borrowed(&c.data),
643        }
644    }
645
646    /// Sanitize this message.
647    ///
648    /// If this message contains HTML, this removes the [tags and attributes] that are not listed in
649    /// the Matrix specification.
650    ///
651    /// It can also optionally remove the [rich reply] fallback from the plain text and HTML
652    /// message. Note that you should be sure that the message is a reply, as there is no way to
653    /// differentiate plain text reply fallbacks and markdown quotes.
654    ///
655    /// This method is only effective on text, notice and emote messages.
656    ///
657    /// [tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
658    /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
659    #[cfg(feature = "html")]
660    pub fn sanitize(
661        &mut self,
662        mode: HtmlSanitizerMode,
663        remove_reply_fallback: RemoveReplyFallback,
664    ) {
665        if let MessageType::Emote(EmoteMessageEventContent { body, formatted, .. })
666        | MessageType::Notice(NoticeMessageEventContent { body, formatted, .. })
667        | MessageType::Text(TextMessageEventContent { body, formatted, .. }) = self
668        {
669            if let Some(formatted) = formatted {
670                formatted.sanitize_html(mode, remove_reply_fallback);
671            }
672            if remove_reply_fallback == RemoveReplyFallback::Yes {
673                *body = remove_plain_reply_fallback(body).to_owned();
674            }
675        }
676    }
677
678    #[track_caller]
679    fn add_reply_fallback(&mut self, original_event: OriginalEventData<'_>) {
680        let empty_formatted_body = || FormattedBody::html(String::new());
681
682        let (body, formatted) = {
683            match self {
684                MessageType::Emote(m) => {
685                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
686                }
687                MessageType::Notice(m) => {
688                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
689                }
690                MessageType::Text(m) => {
691                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
692                }
693                MessageType::Audio(m) => (&mut m.body, None),
694                MessageType::File(m) => (&mut m.body, None),
695                #[cfg(feature = "unstable-msc4274")]
696                MessageType::Gallery(m) => (&mut m.body, None),
697                MessageType::Image(m) => (&mut m.body, None),
698                MessageType::Location(m) => (&mut m.body, None),
699                MessageType::ServerNotice(m) => (&mut m.body, None),
700                MessageType::Video(m) => (&mut m.body, None),
701                MessageType::VerificationRequest(m) => (&mut m.body, None),
702                MessageType::_Custom(m) => (&mut m.body, None),
703            }
704        };
705
706        if let Some(f) = formatted {
707            assert_eq!(
708                f.format,
709                MessageFormat::Html,
710                "can't add reply fallback to non-HTML formatted messages"
711            );
712
713            let formatted_body = &mut f.body;
714
715            (*body, *formatted_body) = reply::plain_and_formatted_reply_body(
716                body.as_str(),
717                (!formatted_body.is_empty()).then_some(formatted_body.as_str()),
718                original_event,
719            );
720        }
721    }
722
723    fn make_replacement_body(&mut self) {
724        let empty_formatted_body = || FormattedBody::html(String::new());
725
726        let (body, formatted) = {
727            match self {
728                MessageType::Emote(m) => {
729                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
730                }
731                MessageType::Notice(m) => {
732                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
733                }
734                MessageType::Text(m) => {
735                    (&mut m.body, Some(m.formatted.get_or_insert_with(empty_formatted_body)))
736                }
737                MessageType::Audio(m) => (&mut m.body, None),
738                MessageType::File(m) => (&mut m.body, None),
739                #[cfg(feature = "unstable-msc4274")]
740                MessageType::Gallery(m) => (&mut m.body, None),
741                MessageType::Image(m) => (&mut m.body, None),
742                MessageType::Location(m) => (&mut m.body, None),
743                MessageType::ServerNotice(m) => (&mut m.body, None),
744                MessageType::Video(m) => (&mut m.body, None),
745                MessageType::VerificationRequest(m) => (&mut m.body, None),
746                MessageType::_Custom(m) => (&mut m.body, None),
747            }
748        };
749
750        // Add replacement fallback.
751        *body = format!("* {body}");
752
753        if let Some(f) = formatted {
754            assert_eq!(
755                f.format,
756                MessageFormat::Html,
757                "make_replacement can't handle non-HTML formatted messages"
758            );
759
760            f.body = format!("* {}", f.body);
761        }
762    }
763}
764
765impl From<MessageType> for RoomMessageEventContent {
766    fn from(msgtype: MessageType) -> Self {
767        Self::new(msgtype)
768    }
769}
770
771impl From<RoomMessageEventContent> for MessageType {
772    fn from(content: RoomMessageEventContent) -> Self {
773        content.msgtype
774    }
775}
776
777/// Metadata about an event to be replaced.
778///
779/// To be used with [`RoomMessageEventContent::make_replacement`].
780#[derive(Debug)]
781pub struct ReplacementMetadata {
782    event_id: OwnedEventId,
783    mentions: Option<Mentions>,
784}
785
786impl ReplacementMetadata {
787    /// Creates a new `ReplacementMetadata` with the given event ID and mentions.
788    pub fn new(event_id: OwnedEventId, mentions: Option<Mentions>) -> Self {
789        Self { event_id, mentions }
790    }
791}
792
793impl From<&OriginalRoomMessageEvent> for ReplacementMetadata {
794    fn from(value: &OriginalRoomMessageEvent) -> Self {
795        ReplacementMetadata::new(value.event_id.to_owned(), value.content.mentions.clone())
796    }
797}
798
799impl From<&OriginalSyncRoomMessageEvent> for ReplacementMetadata {
800    fn from(value: &OriginalSyncRoomMessageEvent) -> Self {
801        ReplacementMetadata::new(value.event_id.to_owned(), value.content.mentions.clone())
802    }
803}
804
805/// The format for the formatted representation of a message body.
806#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
807#[derive(Clone, PartialEq, Eq, StringEnum)]
808#[non_exhaustive]
809pub enum MessageFormat {
810    /// HTML.
811    #[ruma_enum(rename = "org.matrix.custom.html")]
812    Html,
813
814    #[doc(hidden)]
815    _Custom(PrivOwnedStr),
816}
817
818/// Common message event content fields for message types that have separate plain-text and
819/// formatted representations.
820#[derive(Clone, Debug, Deserialize, Serialize)]
821#[allow(clippy::exhaustive_structs)]
822pub struct FormattedBody {
823    /// The format used in the `formatted_body`.
824    pub format: MessageFormat,
825
826    /// The formatted version of the `body`.
827    #[serde(rename = "formatted_body")]
828    pub body: String,
829}
830
831impl FormattedBody {
832    /// Creates a new HTML-formatted message body.
833    pub fn html(body: impl Into<String>) -> Self {
834        Self { format: MessageFormat::Html, body: body.into() }
835    }
836
837    /// Creates a new HTML-formatted message body by parsing the Markdown in `body`.
838    ///
839    /// Returns `None` if no Markdown formatting was found.
840    #[cfg(feature = "markdown")]
841    pub fn markdown(body: impl AsRef<str>) -> Option<Self> {
842        parse_markdown(body.as_ref()).map(Self::html)
843    }
844
845    /// Sanitize this `FormattedBody` if its format is `MessageFormat::Html`.
846    ///
847    /// This removes any [tags and attributes] that are not listed in the Matrix specification.
848    ///
849    /// It can also optionally remove the [rich reply] fallback.
850    ///
851    /// Returns the sanitized HTML if the format is `MessageFormat::Html`.
852    ///
853    /// [tags and attributes]: https://spec.matrix.org/latest/client-server-api/#mroommessage-msgtypes
854    /// [rich reply]: https://spec.matrix.org/latest/client-server-api/#rich-replies
855    #[cfg(feature = "html")]
856    pub fn sanitize_html(
857        &mut self,
858        mode: HtmlSanitizerMode,
859        remove_reply_fallback: RemoveReplyFallback,
860    ) {
861        if self.format == MessageFormat::Html {
862            self.body = sanitize_html(&self.body, mode, remove_reply_fallback);
863        }
864    }
865}
866
867/// The payload for a custom message event.
868#[doc(hidden)]
869#[derive(Clone, Debug, Deserialize, Serialize)]
870pub struct CustomEventContent {
871    /// A custom msgtype.
872    msgtype: String,
873
874    /// The message body.
875    body: String,
876
877    /// Remaining event content.
878    #[serde(flatten)]
879    data: JsonObject,
880}
881
882#[cfg(feature = "markdown")]
883pub(crate) fn parse_markdown(text: &str) -> Option<String> {
884    use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
885
886    const OPTIONS: Options = Options::ENABLE_TABLES.union(Options::ENABLE_STRIKETHROUGH);
887
888    let parser_events: Vec<_> = Parser::new_ext(text, OPTIONS)
889        .map(|event| match event {
890            Event::SoftBreak => Event::HardBreak,
891            _ => event,
892        })
893        .collect();
894
895    // Text that does not contain markdown syntax is always inline because when we encounter several
896    // blocks we convert them to HTML. Inline text is always wrapped by a single paragraph.
897    let first_event_is_paragraph_start =
898        parser_events.first().is_some_and(|event| matches!(event, Event::Start(Tag::Paragraph)));
899    let last_event_is_paragraph_end =
900        parser_events.last().is_some_and(|event| matches!(event, Event::End(TagEnd::Paragraph)));
901    let mut is_inline = first_event_is_paragraph_start && last_event_is_paragraph_end;
902    let mut has_markdown = !is_inline;
903
904    if !has_markdown {
905        // Check whether the events contain other blocks and whether they contain inline markdown
906        // syntax.
907        let mut pos = 0;
908
909        for event in parser_events.iter().skip(1) {
910            match event {
911                Event::Text(s) => {
912                    // If the string does not contain markdown, the only modification that should
913                    // happen is that newlines are converted to hardbreaks. It means that we should
914                    // find all the other characters from the original string in the text events.
915                    // Let's check that by walking the original string.
916                    if text[pos..].starts_with(s.as_ref()) {
917                        pos += s.len();
918                        continue;
919                    }
920                }
921                Event::HardBreak => {
922                    // A hard break happens when a newline is encountered, which is not necessarily
923                    // markdown syntax. Skip the newline in the original string for the walking
924                    // above to work.
925                    if text[pos..].starts_with("\r\n") {
926                        pos += 2;
927                        continue;
928                    } else if text[pos..].starts_with(['\r', '\n']) {
929                        pos += 1;
930                        continue;
931                    }
932                }
933                // A paragraph end is fine because we would detect markdown from the paragraph
934                // start.
935                Event::End(TagEnd::Paragraph) => continue,
936                // Any other event means there is markdown syntax.
937                Event::Start(tag) => {
938                    is_inline &= !is_block_tag(tag);
939                }
940                _ => {}
941            }
942
943            has_markdown = true;
944
945            // Stop when we also know that there are several blocks.
946            if !is_inline {
947                break;
948            }
949        }
950
951        // If we are not at the end of the string, some characters were removed.
952        has_markdown |= pos != text.len();
953    }
954
955    // If the string does not contain markdown, don't generate HTML.
956    if !has_markdown {
957        return None;
958    }
959
960    let mut events_iter = parser_events.into_iter();
961
962    // If the content is inline, remove the wrapping paragraph, as instructed by the Matrix spec.
963    if is_inline {
964        events_iter.next();
965        events_iter.next_back();
966    }
967
968    let mut html_body = String::new();
969    pulldown_cmark::html::push_html(&mut html_body, events_iter);
970
971    Some(html_body)
972}
973
974/// Whether the given tag is a block HTML element.
975#[cfg(feature = "markdown")]
976fn is_block_tag(tag: &pulldown_cmark::Tag<'_>) -> bool {
977    use pulldown_cmark::Tag;
978
979    matches!(
980        tag,
981        Tag::Paragraph
982            | Tag::Heading { .. }
983            | Tag::BlockQuote(_)
984            | Tag::CodeBlock(_)
985            | Tag::HtmlBlock
986            | Tag::List(_)
987            | Tag::FootnoteDefinition(_)
988            | Tag::Table(_)
989    )
990}
991
992#[cfg(all(test, feature = "markdown"))]
993mod tests {
994    use super::parse_markdown;
995
996    #[test]
997    fn detect_markdown() {
998        // Simple single-line text.
999        let text = "Hello world.";
1000        assert_eq!(parse_markdown(text), None);
1001
1002        // Simple double-line text.
1003        let text = "Hello\nworld.";
1004        assert_eq!(parse_markdown(text), None);
1005
1006        // With new paragraph.
1007        let text = "Hello\n\nworld.";
1008        assert_eq!(parse_markdown(text).as_deref(), Some("<p>Hello</p>\n<p>world.</p>\n"));
1009
1010        // With heading and paragraph.
1011        let text = "## Hello\n\nworld.";
1012        assert_eq!(parse_markdown(text).as_deref(), Some("<h2>Hello</h2>\n<p>world.</p>\n"));
1013
1014        // With paragraph and code block.
1015        let text = "Hello\n\n```\nworld.\n```";
1016        assert_eq!(
1017            parse_markdown(text).as_deref(),
1018            Some("<p>Hello</p>\n<pre><code>world.\n</code></pre>\n")
1019        );
1020
1021        // With tagged element.
1022        let text = "Hello **world**.";
1023        assert_eq!(parse_markdown(text).as_deref(), Some("Hello <strong>world</strong>."));
1024
1025        // Containing backslash escapes.
1026        let text = r#"Hello \<world\>."#;
1027        assert_eq!(parse_markdown(text).as_deref(), Some("Hello &lt;world&gt;."));
1028
1029        // Starting with backslash escape.
1030        let text = r#"\> Hello world."#;
1031        assert_eq!(parse_markdown(text).as_deref(), Some("&gt; Hello world."));
1032
1033        // With entity reference.
1034        let text = r#"Hello &lt;world&gt;."#;
1035        assert_eq!(parse_markdown(text).as_deref(), Some("Hello &lt;world&gt;."));
1036
1037        // With numeric reference.
1038        let text = "Hello w&#8853;rld.";
1039        assert_eq!(parse_markdown(text).as_deref(), Some("Hello w⊕rld."));
1040    }
1041
1042    #[test]
1043    fn detect_commonmark() {
1044        // Examples from the CommonMark spec.
1045
1046        let text = r#"\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~"#;
1047        assert_eq!(
1048            parse_markdown(text).as_deref(),
1049            Some(r##"!"#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~"##)
1050        );
1051
1052        let text = r#"\→\A\a\ \3\φ\«"#;
1053        assert_eq!(parse_markdown(text).as_deref(), None);
1054
1055        let text = r#"\*not emphasized*"#;
1056        assert_eq!(parse_markdown(text).as_deref(), Some("*not emphasized*"));
1057
1058        let text = r#"\<br/> not a tag"#;
1059        assert_eq!(parse_markdown(text).as_deref(), Some("&lt;br/&gt; not a tag"));
1060
1061        let text = r#"\[not a link](/foo)"#;
1062        assert_eq!(parse_markdown(text).as_deref(), Some("[not a link](/foo)"));
1063
1064        let text = r#"\`not code`"#;
1065        assert_eq!(parse_markdown(text).as_deref(), Some("`not code`"));
1066
1067        let text = r#"1\. not a list"#;
1068        assert_eq!(parse_markdown(text).as_deref(), Some("1. not a list"));
1069
1070        let text = r#"\* not a list"#;
1071        assert_eq!(parse_markdown(text).as_deref(), Some("* not a list"));
1072
1073        let text = r#"\# not a heading"#;
1074        assert_eq!(parse_markdown(text).as_deref(), Some("# not a heading"));
1075
1076        let text = r#"\[foo]: /url "not a reference""#;
1077        assert_eq!(parse_markdown(text).as_deref(), Some(r#"[foo]: /url "not a reference""#));
1078
1079        let text = r#"\&ouml; not a character entity"#;
1080        assert_eq!(parse_markdown(text).as_deref(), Some("&amp;ouml; not a character entity"));
1081
1082        let text = r#"\\*emphasis*"#;
1083        assert_eq!(parse_markdown(text).as_deref(), Some(r#"\<em>emphasis</em>"#));
1084
1085        let text = "foo\\\nbar";
1086        assert_eq!(parse_markdown(text).as_deref(), Some("foo<br />\nbar"));
1087
1088        let text = " ***\n  ***\n   ***";
1089        assert_eq!(parse_markdown(text).as_deref(), Some("<hr />\n<hr />\n<hr />\n"));
1090
1091        let text = "Foo\n***\nbar";
1092        assert_eq!(parse_markdown(text).as_deref(), Some("<p>Foo</p>\n<hr />\n<p>bar</p>\n"));
1093
1094        let text = "</div>\n*foo*";
1095        assert_eq!(parse_markdown(text).as_deref(), Some("</div>\n*foo*"));
1096
1097        let text = "<div>\n*foo*\n\n*bar*";
1098        assert_eq!(parse_markdown(text).as_deref(), Some("<div>\n*foo*\n<p><em>bar</em></p>\n"));
1099
1100        let text = "aaa\nbbb\n\nccc\nddd";
1101        assert_eq!(
1102            parse_markdown(text).as_deref(),
1103            Some("<p>aaa<br />\nbbb</p>\n<p>ccc<br />\nddd</p>\n")
1104        );
1105
1106        let text = "  aaa\n bbb";
1107        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb"));
1108
1109        let text = "aaa\n             bbb\n                                       ccc";
1110        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb<br />\nccc"));
1111
1112        let text = "aaa     \nbbb     ";
1113        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb"));
1114    }
1115}