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.19/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.19/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.19/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.19/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.19/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.19/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.19/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    /// Get the thread relation from this content, if any.
266    pub fn thread(&self) -> Option<&Thread> {
267        self.relates_to.as_ref().and_then(as_variant!(Relation::Thread))
268    }
269
270    /// Apply the given new content from a [`Replacement`] to this message.
271    ///
272    /// [`Replacement`]: crate::relation::Replacement
273    pub fn apply_replacement(&mut self, new_content: RoomMessageEventContentWithoutRelation) {
274        let RoomMessageEventContentWithoutRelation {
275            msgtype,
276            mentions,
277            #[cfg(feature = "unstable-msc4471")]
278            stream,
279        } = new_content;
280        self.msgtype = msgtype;
281        self.mentions = mentions;
282        #[cfg(feature = "unstable-msc4471")]
283        {
284            self.stream = stream;
285        }
286    }
287
288    /// Sanitize this message.
289    ///
290    /// If this message contains HTML, this removes the [tags and attributes] that are not listed in
291    /// the Matrix specification.
292    ///
293    /// It can also optionally remove the [rich reply] fallback from the plain text and HTML
294    /// message.
295    ///
296    /// This method is only effective on text, notice and emote messages.
297    ///
298    /// [tags and attributes]: https://spec.matrix.org/v1.19/client-server-api/#mroommessage-msgtypes
299    /// [rich reply]: https://spec.matrix.org/v1.19/client-server-api/#rich-replies
300    #[cfg(feature = "html")]
301    pub fn sanitize(
302        &mut self,
303        mode: HtmlSanitizerMode,
304        remove_reply_fallback: RemoveReplyFallback,
305    ) {
306        let remove_reply_fallback = if matches!(self.relates_to, Some(Relation::Reply(_))) {
307            remove_reply_fallback
308        } else {
309            RemoveReplyFallback::No
310        };
311
312        self.msgtype.sanitize(mode, remove_reply_fallback);
313    }
314
315    fn without_relation(self) -> RoomMessageEventContentWithoutRelation {
316        if self.relates_to.is_some() {
317            warn!("Overwriting existing relates_to value");
318        }
319
320        self.into()
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.19/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.19/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.19/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.19/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.19/client-server-api/#mroommessage-msgtypes
610    /// [rich reply]: https://spec.matrix.org/v1.19/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 (body, formatted) = {
632            match self {
633                MessageType::Emote(m) => (&mut m.body, m.formatted.as_mut()),
634                MessageType::Notice(m) => (&mut m.body, m.formatted.as_mut()),
635                MessageType::Text(m) => (&mut m.body, m.formatted.as_mut()),
636                MessageType::Audio(m) => (&mut m.body, None),
637                MessageType::File(m) => (&mut m.body, None),
638                #[cfg(feature = "unstable-msc4274")]
639                MessageType::Gallery(m) => (&mut m.body, None),
640                MessageType::Image(m) => (&mut m.body, None),
641                MessageType::Location(m) => (&mut m.body, None),
642                MessageType::ServerNotice(m) => (&mut m.body, None),
643                MessageType::Video(m) => (&mut m.body, None),
644                MessageType::VerificationRequest(m) => (&mut m.body, None),
645                MessageType::_Custom(m) => (&mut m.body, None),
646            }
647        };
648
649        // Add replacement fallback.
650        *body = format!("* {body}");
651
652        if let Some(f) = formatted {
653            assert_eq!(
654                f.format,
655                MessageFormat::Html,
656                "make_replacement can't handle non-HTML formatted messages"
657            );
658
659            f.body = format!("* {}", f.body);
660        }
661    }
662}
663
664impl From<MessageType> for RoomMessageEventContent {
665    fn from(msgtype: MessageType) -> Self {
666        Self::new(msgtype)
667    }
668}
669
670impl From<RoomMessageEventContent> for MessageType {
671    fn from(content: RoomMessageEventContent) -> Self {
672        content.msgtype
673    }
674}
675
676/// Metadata about an event to be replaced.
677///
678/// To be used with [`RoomMessageEventContent::make_replacement`].
679#[derive(Debug)]
680pub struct ReplacementMetadata {
681    event_id: OwnedEventId,
682    mentions: Option<Mentions>,
683}
684
685impl ReplacementMetadata {
686    /// Creates a new `ReplacementMetadata` with the given event ID and mentions.
687    pub fn new(event_id: OwnedEventId, mentions: Option<Mentions>) -> Self {
688        Self { event_id, mentions }
689    }
690}
691
692impl From<&OriginalRoomMessageEvent> for ReplacementMetadata {
693    fn from(value: &OriginalRoomMessageEvent) -> Self {
694        ReplacementMetadata::new(value.event_id.to_owned(), value.content.mentions.clone())
695    }
696}
697
698impl From<&OriginalSyncRoomMessageEvent> for ReplacementMetadata {
699    fn from(value: &OriginalSyncRoomMessageEvent) -> Self {
700        ReplacementMetadata::new(value.event_id.to_owned(), value.content.mentions.clone())
701    }
702}
703
704/// Metadata about an event to reply to or to add to a thread.
705///
706/// To be used with [`RoomMessageEventContent::make_reply_to`] or
707/// [`RoomMessageEventContent::make_for_thread`].
708#[derive(Clone, Copy, Debug)]
709pub struct ReplyMetadata<'a> {
710    /// The event ID of the event to reply to.
711    event_id: &'a EventId,
712    /// The sender of the event to reply to.
713    sender: &'a UserId,
714    /// The `m.thread` relation of the event to reply to, if any.
715    thread: Option<&'a Thread>,
716}
717
718impl<'a> ReplyMetadata<'a> {
719    /// Creates a new `ReplyMetadata` with the given event ID, sender and thread relation.
720    pub fn new(event_id: &'a EventId, sender: &'a UserId, thread: Option<&'a Thread>) -> Self {
721        Self { event_id, sender, thread }
722    }
723}
724
725impl<'a> From<&'a OriginalRoomMessageEvent> for ReplyMetadata<'a> {
726    fn from(value: &'a OriginalRoomMessageEvent) -> Self {
727        ReplyMetadata::new(&value.event_id, &value.sender, value.content.thread())
728    }
729}
730
731impl<'a> From<&'a OriginalSyncRoomMessageEvent> for ReplyMetadata<'a> {
732    fn from(value: &'a OriginalSyncRoomMessageEvent) -> Self {
733        ReplyMetadata::new(&value.event_id, &value.sender, value.content.thread())
734    }
735}
736
737/// The format for the formatted representation of a message body.
738#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
739#[derive(Clone, StringEnum)]
740#[non_exhaustive]
741pub enum MessageFormat {
742    /// HTML.
743    #[ruma_enum(rename = "org.matrix.custom.html")]
744    Html,
745
746    #[doc(hidden)]
747    _Custom(PrivOwnedStr),
748}
749
750/// Common message event content fields for message types that have separate plain-text and
751/// formatted representations.
752#[derive(Clone, Debug, Deserialize, Serialize)]
753#[allow(clippy::exhaustive_structs)]
754pub struct FormattedBody {
755    /// The format used in the `formatted_body`.
756    pub format: MessageFormat,
757
758    /// The formatted version of the `body`.
759    #[serde(rename = "formatted_body")]
760    pub body: String,
761}
762
763impl FormattedBody {
764    /// Creates a new HTML-formatted message body.
765    pub fn html(body: impl Into<String>) -> Self {
766        Self { format: MessageFormat::Html, body: body.into() }
767    }
768
769    /// Creates a new HTML-formatted message body by parsing the Markdown in `body`.
770    ///
771    /// Returns `None` if no Markdown formatting was found.
772    #[cfg(feature = "markdown")]
773    pub fn markdown(body: impl AsRef<str>) -> Option<Self> {
774        parse_markdown(body.as_ref()).map(Self::html)
775    }
776
777    /// Sanitize this `FormattedBody` if its format is `MessageFormat::Html`.
778    ///
779    /// This removes any [tags and attributes] that are not listed in the Matrix specification.
780    ///
781    /// It can also optionally remove the [rich reply] fallback.
782    ///
783    /// Returns the sanitized HTML if the format is `MessageFormat::Html`.
784    ///
785    /// [tags and attributes]: https://spec.matrix.org/v1.19/client-server-api/#mroommessage-msgtypes
786    /// [rich reply]: https://spec.matrix.org/v1.19/client-server-api/#rich-replies
787    #[cfg(feature = "html")]
788    pub fn sanitize_html(
789        &mut self,
790        mode: HtmlSanitizerMode,
791        remove_reply_fallback: RemoveReplyFallback,
792    ) {
793        if self.format == MessageFormat::Html {
794            self.body = sanitize_html(&self.body, mode, remove_reply_fallback);
795        }
796    }
797}
798
799/// The payload for a custom message event.
800#[doc(hidden)]
801#[derive(Clone, Debug, Serialize)]
802pub struct CustomMessageContent {
803    /// A custom msgtype.
804    msgtype: String,
805
806    /// The message body.
807    body: String,
808
809    /// Remaining event content.
810    #[serde(flatten)]
811    data: JsonObject,
812}
813
814#[cfg(feature = "markdown")]
815pub(crate) fn parse_markdown(text: &str) -> Option<String> {
816    use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
817
818    const OPTIONS: Options = Options::ENABLE_TABLES.union(Options::ENABLE_STRIKETHROUGH);
819
820    let parser_events: Vec<_> = Parser::new_ext(text, OPTIONS)
821        .map(|event| match event {
822            Event::SoftBreak => Event::HardBreak,
823            _ => event,
824        })
825        .collect();
826
827    // Text that does not contain markdown syntax is always inline because when we encounter several
828    // blocks we convert them to HTML. Inline text is always wrapped by a single paragraph.
829    let first_event_is_paragraph_start =
830        parser_events.first().is_some_and(|event| matches!(event, Event::Start(Tag::Paragraph)));
831    let last_event_is_paragraph_end =
832        parser_events.last().is_some_and(|event| matches!(event, Event::End(TagEnd::Paragraph)));
833    let mut is_inline = first_event_is_paragraph_start && last_event_is_paragraph_end;
834    let mut has_markdown = !is_inline;
835
836    if !has_markdown {
837        // Check whether the events contain other blocks and whether they contain inline markdown
838        // syntax.
839        let mut pos = 0;
840
841        for event in parser_events.iter().skip(1) {
842            match event {
843                // If the string does not contain markdown, the only modification that should
844                // happen is that newlines are converted to hardbreaks. It means that we should
845                // find all the other characters from the original string in the text events.
846                // Let's check that by walking the original string.
847                Event::Text(s) if text[pos..].starts_with(s.as_ref()) => {
848                    pos += s.len();
849                    continue;
850                }
851                Event::HardBreak => {
852                    // A hard break happens when a newline is encountered, which is not necessarily
853                    // markdown syntax. Skip the newline in the original string for the walking
854                    // above to work.
855                    if text[pos..].starts_with("\r\n") {
856                        pos += 2;
857                        continue;
858                    } else if text[pos..].starts_with(['\r', '\n']) {
859                        pos += 1;
860                        continue;
861                    }
862                }
863                // A paragraph end is fine because we would detect markdown from the paragraph
864                // start.
865                Event::End(TagEnd::Paragraph) => continue,
866                // Any other event means there is markdown syntax.
867                Event::Start(tag) => {
868                    is_inline &= !is_block_tag(tag);
869                }
870                _ => {}
871            }
872
873            has_markdown = true;
874
875            // Stop when we also know that there are several blocks.
876            if !is_inline {
877                break;
878            }
879        }
880
881        // If we are not at the end of the string, some characters were removed.
882        has_markdown |= pos != text.len();
883    }
884
885    // If the string does not contain markdown, don't generate HTML.
886    if !has_markdown {
887        return None;
888    }
889
890    let mut events_iter = parser_events.into_iter();
891
892    // If the content is inline, remove the wrapping paragraph, as instructed by the Matrix spec.
893    if is_inline {
894        events_iter.next();
895        events_iter.next_back();
896    }
897
898    let mut html_body = String::new();
899    pulldown_cmark::html::push_html(&mut html_body, events_iter);
900
901    Some(html_body)
902}
903
904/// Whether the given tag is a block HTML element.
905#[cfg(feature = "markdown")]
906fn is_block_tag(tag: &pulldown_cmark::Tag<'_>) -> bool {
907    use pulldown_cmark::Tag;
908
909    matches!(
910        tag,
911        Tag::Paragraph
912            | Tag::Heading { .. }
913            | Tag::BlockQuote(_)
914            | Tag::CodeBlock(_)
915            | Tag::HtmlBlock
916            | Tag::List(_)
917            | Tag::FootnoteDefinition(_)
918            | Tag::Table(_)
919    )
920}
921
922#[cfg(all(test, feature = "markdown"))]
923mod tests {
924    use super::parse_markdown;
925
926    #[test]
927    fn detect_markdown() {
928        // Simple single-line text.
929        let text = "Hello world.";
930        assert_eq!(parse_markdown(text), None);
931
932        // Simple double-line text.
933        let text = "Hello\nworld.";
934        assert_eq!(parse_markdown(text), None);
935
936        // With new paragraph.
937        let text = "Hello\n\nworld.";
938        assert_eq!(parse_markdown(text).as_deref(), Some("<p>Hello</p>\n<p>world.</p>\n"));
939
940        // With heading and paragraph.
941        let text = "## Hello\n\nworld.";
942        assert_eq!(parse_markdown(text).as_deref(), Some("<h2>Hello</h2>\n<p>world.</p>\n"));
943
944        // With paragraph and code block.
945        let text = "Hello\n\n```\nworld.\n```";
946        assert_eq!(
947            parse_markdown(text).as_deref(),
948            Some("<p>Hello</p>\n<pre><code>world.\n</code></pre>\n")
949        );
950
951        // With tagged element.
952        let text = "Hello **world**.";
953        assert_eq!(parse_markdown(text).as_deref(), Some("Hello <strong>world</strong>."));
954
955        // Containing backslash escapes.
956        let text = r#"Hello \<world\>."#;
957        assert_eq!(parse_markdown(text).as_deref(), Some("Hello &lt;world&gt;."));
958
959        // Starting with backslash escape.
960        let text = r#"\> Hello world."#;
961        assert_eq!(parse_markdown(text).as_deref(), Some("&gt; Hello world."));
962
963        // With entity reference.
964        let text = r#"Hello &lt;world&gt;."#;
965        assert_eq!(parse_markdown(text).as_deref(), Some("Hello &lt;world&gt;."));
966
967        // With numeric reference.
968        let text = "Hello w&#8853;rld.";
969        assert_eq!(parse_markdown(text).as_deref(), Some("Hello w⊕rld."));
970    }
971
972    #[test]
973    fn detect_commonmark() {
974        // Examples from the CommonMark spec.
975
976        let text = r#"\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~"#;
977        assert_eq!(
978            parse_markdown(text).as_deref(),
979            Some(r##"!"#$%&amp;'()*+,-./:;&lt;=&gt;?@[\]^_`{|}~"##)
980        );
981
982        let text = r#"\→\A\a\ \3\φ\«"#;
983        assert_eq!(parse_markdown(text).as_deref(), None);
984
985        let text = r#"\*not emphasized*"#;
986        assert_eq!(parse_markdown(text).as_deref(), Some("*not emphasized*"));
987
988        let text = r#"\<br/> not a tag"#;
989        assert_eq!(parse_markdown(text).as_deref(), Some("&lt;br/&gt; not a tag"));
990
991        let text = r#"\[not a link](/foo)"#;
992        assert_eq!(parse_markdown(text).as_deref(), Some("[not a link](/foo)"));
993
994        let text = r#"\`not code`"#;
995        assert_eq!(parse_markdown(text).as_deref(), Some("`not code`"));
996
997        let text = r#"1\. not a list"#;
998        assert_eq!(parse_markdown(text).as_deref(), Some("1. not a list"));
999
1000        let text = r#"\* not a list"#;
1001        assert_eq!(parse_markdown(text).as_deref(), Some("* not a list"));
1002
1003        let text = r#"\# not a heading"#;
1004        assert_eq!(parse_markdown(text).as_deref(), Some("# not a heading"));
1005
1006        let text = r#"\[foo]: /url "not a reference""#;
1007        assert_eq!(parse_markdown(text).as_deref(), Some(r#"[foo]: /url "not a reference""#));
1008
1009        let text = r#"\&ouml; not a character entity"#;
1010        assert_eq!(parse_markdown(text).as_deref(), Some("&amp;ouml; not a character entity"));
1011
1012        let text = r#"\\*emphasis*"#;
1013        assert_eq!(parse_markdown(text).as_deref(), Some(r#"\<em>emphasis</em>"#));
1014
1015        let text = "foo\\\nbar";
1016        assert_eq!(parse_markdown(text).as_deref(), Some("foo<br />\nbar"));
1017
1018        let text = " ***\n  ***\n   ***";
1019        assert_eq!(parse_markdown(text).as_deref(), Some("<hr />\n<hr />\n<hr />\n"));
1020
1021        let text = "Foo\n***\nbar";
1022        assert_eq!(parse_markdown(text).as_deref(), Some("<p>Foo</p>\n<hr />\n<p>bar</p>\n"));
1023
1024        let text = "</div>\n*foo*";
1025        assert_eq!(parse_markdown(text).as_deref(), Some("</div>\n*foo*"));
1026
1027        let text = "<div>\n*foo*\n\n*bar*";
1028        assert_eq!(parse_markdown(text).as_deref(), Some("<div>\n*foo*\n<p><em>bar</em></p>\n"));
1029
1030        let text = "aaa\nbbb\n\nccc\nddd";
1031        assert_eq!(
1032            parse_markdown(text).as_deref(),
1033            Some("<p>aaa<br />\nbbb</p>\n<p>ccc<br />\nddd</p>\n")
1034        );
1035
1036        let text = "  aaa\n bbb";
1037        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb"));
1038
1039        let text = "aaa\n             bbb\n                                       ccc";
1040        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb<br />\nccc"));
1041
1042        let text = "aaa     \nbbb     ";
1043        assert_eq!(parse_markdown(text).as_deref(), Some("aaa<br />\nbbb"));
1044    }
1045}