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