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