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