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