//! Types for extensible text message events ([MSC1767]). //! //! # Extensible events //! //! MSCs [1767] (Text, Emote and Notice), [3551] (Files), [3552] (Images and Stickers), [3553] //! (Videos), [3246] (Audio), and [3488] (Location) introduce new primary types called extensible //! events. These types are meant to replace the `m.room.message` primary type and its `msgtype`s. //! Other MSCs introduce new types with an `m.room.message` fallback, like [MSC3245] (Voice //! Messages), and types that only have an extensible events format, like [MSC3381] (Polls). //! //! # Transition Period //! //! MSC1767 defines a transition period that will start after the extensible events are released in //! a Matrix version. It should last approximately one year, but the end of that period will be //! formalized in a new Matrix version. //! //! The new primary types should not be sent over the Matrix network before the end of the //! transition period. Instead, transitional `m.room.message` events should be sent. These //! transitional events include the content of the now legacy `m.room.message` event and the content //! of the new extensible event types in a single event. //! //! # How to use them //! //! First, you can enable the `unstable-extensible-events` feature from the `ruma` crate, that //! will enable all the MSCs for the extensible events that correspond to the legacy `msgtype`s //! (1767, 3246, 3488, 3551, 3552, 3553). It is also possible to enable only the MSCs you want with //! the `unstable-mscXXXX` features (where `XXXX` is the number of the MSC). //! //! The recommended way to send transitional extensible events while they are unstable and during //! the transition period is to use the constructors and helper methods of //! [`RoomMessageEventContent`]. The data will be duplicated in both the legacy and extensible //! events fields as needed. //! //! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 //! [1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 //! [3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551 //! [3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552 //! [3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553 //! [3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246 //! [3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488 //! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245 //! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381 //! [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent use std::ops::Deref; use ruma_macros::EventContent; use serde::{Deserialize, Serialize}; use thiserror::Error; pub(crate) mod content_serde; use content_serde::MessageContentSerDeHelper; use super::room::message::Relation; /// The payload for an extensible text message. /// /// This is the new primary type introduced in [MSC1767] and should not be sent before the end of /// the transition period. See the documentation of the [`message`] module for more information. /// /// To construct a `MessageEventContent` with a custom [`MessageContent`], convert it with /// `MessageEventContent::from()` / `.into()`. /// /// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767 /// [`message`]: super::message #[derive(Clone, Debug, Serialize, Deserialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.message", kind = MessageLike, without_relation)] pub struct MessageEventContent { /// The message's text content. #[serde(flatten)] pub message: MessageContent, /// Information about related messages. #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl MessageEventContent { /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { Self { message: MessageContent::plain(body), relates_to: None } } /// A convenience constructor to create an HTML message. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self { message: MessageContent::html(body, html_body), relates_to: None } } /// A convenience constructor to create a Markdown message. /// /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { Self { message: MessageContent::markdown(body), relates_to: None } } } impl From for MessageEventContent { fn from(message: MessageContent) -> Self { Self { message, relates_to: None } } } /// Text message content. /// /// A `MessageContent` must contain at least one message to be used as a fallback text /// representation. /// /// To construct a `MessageContent` with custom MIME types, construct a `Vec` first and use /// its `.try_from()` / `.try_into()` implementation that will only fail if the `Vec` is empty. #[derive(Clone, Debug, Deserialize)] #[serde(try_from = "MessageContentSerDeHelper")] pub struct MessageContent(pub(crate) Vec); impl MessageContent { /// Create a `MessageContent` from an array of messages. /// /// Returns `None` if the array is empty. pub fn new(messages: Vec) -> Option { if messages.is_empty() { None } else { Some(Self(messages)) } } /// A convenience constructor to create a plain text message. pub fn plain(body: impl Into) -> Self { Self(vec![Text::plain(body)]) } /// A convenience constructor to create an HTML message. pub fn html(body: impl Into, html_body: impl Into) -> Self { Self(vec![Text::html(html_body), Text::plain(body)]) } /// A convenience constructor to create a Markdown message. /// /// Returns an HTML message if some Markdown formatting was detected, otherwise returns a plain /// text message. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef + Into) -> Self { let mut message = Vec::with_capacity(2); if let Some(html_body) = Text::markdown(&body) { message.push(html_body); } message.push(Text::plain(body)); Self(message) } /// Get the plain text representation of this message. pub fn find_plain(&self) -> Option<&str> { self.iter() .find(|content| content.mimetype == "text/plain") .map(|content| content.body.as_ref()) } /// Get the HTML representation of this message. pub fn find_html(&self) -> Option<&str> { self.iter() .find(|content| content.mimetype == "text/html") .map(|content| content.body.as_ref()) } } /// The error type returned when trying to construct an empty `MessageContent`. #[derive(Debug, Error)] #[non_exhaustive] #[error("MessageContent cannot be empty")] pub struct EmptyMessageContentError; impl TryFrom> for MessageContent { type Error = EmptyMessageContentError; fn try_from(messages: Vec) -> Result { Self::new(messages).ok_or(EmptyMessageContentError) } } impl Deref for MessageContent { type Target = [Text]; fn deref(&self) -> &Self::Target { &self.0 } } /// Text message content. #[derive(Clone, Debug, Serialize, Deserialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Text { /// The MIME type of the `body`. /// /// This must follow the format defined in [RFC6838]. /// /// [RFC6838]: https://datatracker.ietf.org/doc/html/rfc6838 #[serde(default = "Text::default_mimetype")] pub mimetype: String, /// The text content. pub body: String, /// The language of the text ([MSC3554]). /// /// This must be a valid language code according to [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt). /// /// [MSC3554]: https://github.com/matrix-org/matrix-spec-proposals/pull/3554 #[cfg(feature = "unstable-msc3554")] #[serde(skip_serializing_if = "Option::is_none")] pub lang: Option, } impl Text { /// Creates a new `Text` with the given MIME type and body. pub fn new(mimetype: impl Into, body: impl Into) -> Self { Self { mimetype: mimetype.into(), body: body.into(), #[cfg(feature = "unstable-msc3554")] lang: None, } } /// Creates a new plain text message body. pub fn plain(body: impl Into) -> Self { Self::new("text/plain", body) } /// Creates a new HTML-formatted message body. pub fn html(body: impl Into) -> Self { Self::new("text/html", body) } /// Creates a new HTML-formatted message body by parsing the Markdown in `body`. /// /// Returns `None` if no Markdown formatting was found. #[cfg(feature = "markdown")] pub fn markdown(body: impl AsRef) -> Option { let body = body.as_ref(); let mut html_body = String::new(); pulldown_cmark::html::push_html(&mut html_body, pulldown_cmark::Parser::new(body)); (html_body != format!("

{}

\n", body)).then(|| Self::html(html_body)) } fn default_mimetype() -> String { "text/plain".to_owned() } } /// The error type returned when a conversion to an extensible event type fails. #[derive(Debug, Error)] #[non_exhaustive] pub enum TryFromExtensibleError { /// A field is missing. #[error("missing field `{0}`")] MissingField(String), }