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