Skip to main content

ruma_events/room/
message.rs

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