//! Types for the [`m.room.message`] event. //! //! [`m.room.message`]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage use std::{borrow::Cow, fmt}; use ruma_macros::EventContent; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value as JsonValue; use crate::{ serde::{JsonObject, StringEnum}, OwnedEventId, PrivOwnedStr, }; mod audio; mod content_serde; mod emote; mod file; mod image; mod key_verification_request; mod location; mod notice; mod relation_serde; mod reply; mod server_notice; mod text; mod video; pub use audio::{AudioInfo, AudioMessageEventContent}; pub use emote::EmoteMessageEventContent; pub use file::{FileInfo, FileMessageEventContent}; pub use image::ImageMessageEventContent; pub use key_verification_request::KeyVerificationRequestEventContent; pub use location::{LocationInfo, LocationMessageEventContent}; pub use notice::NoticeMessageEventContent; pub use server_notice::{LimitType, ServerNoticeMessageEventContent, ServerNoticeType}; pub use text::TextMessageEventContent; pub use video::{VideoInfo, VideoMessageEventContent}; /// The content of an `m.room.message` event. /// /// This event is used when sending messages in a room. /// /// Messages are not limited to be text. #[derive(Clone, Debug, Serialize, EventContent)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] #[ruma_event(type = "m.room.message", kind = MessageLike)] pub struct RoomMessageEventContent { /// A key which identifies the type of message being sent. /// /// This also holds the specific content of each message. #[serde(flatten)] pub msgtype: MessageType, /// Information about related messages for [rich replies]. /// /// [rich replies]: https://spec.matrix.org/v1.2/client-server-api/#rich-replies #[serde(flatten, skip_serializing_if = "Option::is_none")] pub relates_to: Option, } impl RoomMessageEventContent { /// Create a `RoomMessageEventContent` with the given `MessageType`. pub fn new(msgtype: MessageType) -> Self { Self { msgtype, relates_to: None } } /// A constructor to create a plain text message. pub fn text_plain(body: impl Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::plain(body))) } /// A constructor to create an html message. pub fn text_html(body: impl Into, html_body: impl Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::html(body, html_body))) } /// A constructor to create a markdown message. #[cfg(feature = "markdown")] pub fn text_markdown(body: impl AsRef + Into) -> Self { Self::new(MessageType::Text(TextMessageEventContent::markdown(body))) } /// A constructor to create a plain text notice. pub fn notice_plain(body: impl Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::plain(body))) } /// A constructor to create an html notice. pub fn notice_html(body: impl Into, html_body: impl Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::html(body, html_body))) } /// A constructor to create a markdown notice. #[cfg(feature = "markdown")] pub fn notice_markdown(body: impl AsRef + Into) -> Self { Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body))) } /// Creates a plain text reply to a message. /// /// This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to /// the previous message, for which the room ID is required. If you want to reply to an /// [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling /// [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. pub fn text_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { let formatted: Option<&str> = None; let (body, html_body) = reply::plain_and_formatted_reply_body(reply, formatted, original_message); Self { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, }), ..Self::text_html(body, html_body) } } /// Creates a html text reply to a message. /// /// This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to /// the previous message, for which the room ID is required. If you want to reply to an /// [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling /// [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. pub fn text_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { let (body, html_body) = reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); Self { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, }), ..Self::text_html(body, html_body) } } /// Creates a plain text notice reply to a message. /// /// This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to /// the previous message, for which the room ID is required. If you want to reply to an /// [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling /// [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. pub fn notice_reply_plain( reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { let formatted: Option<&str> = None; let (body, html_body) = reply::plain_and_formatted_reply_body(reply, formatted, original_message); Self { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.to_owned() }, }), ..Self::notice_html(body, html_body) } } /// Creates a html text notice reply to a message. /// /// This constructor requires an [`OriginalRoomMessageEvent`] since it creates a permalink to /// the previous message, for which the room ID is required. If you want to reply to an /// [`OriginalSyncRoomMessageEvent`], you have to convert it first by calling /// [`.into_full_event()`][crate::events::OriginalSyncMessageLikeEvent::into_full_event]. pub fn notice_reply_html( reply: impl fmt::Display, html_reply: impl fmt::Display, original_message: &OriginalRoomMessageEvent, ) -> Self { let (body, html_body) = reply::plain_and_formatted_reply_body(reply, Some(html_reply), original_message); Self { relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, }), ..Self::notice_html(body, html_body) } } /// Create a new reply with the given message and optionally forwards the [`Relation::Thread`]. /// /// If `message` is a text, an emote or a notice message, it is modified to include the rich /// reply fallback. #[cfg(feature = "unstable-msc3440")] pub fn reply( message: MessageType, original_message: &OriginalRoomMessageEvent, forward_thread: ForwardThread, ) -> Self { let make_reply = |body, formatted: Option| { reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), original_message) }; let msgtype = match message { MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Text(TextMessageEventContent::html(body, html_body)) } MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Emote(EmoteMessageEventContent::html(body, html_body)) } MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) } _ => message, }; let relates_to = if let Some(Relation::Thread(Thread { event_id, .. })) = original_message .content .relates_to .as_ref() .filter(|_| forward_thread == ForwardThread::Yes) { Relation::Thread(Thread::reply(event_id.clone(), original_message.event_id.clone())) } else { Relation::Reply { in_reply_to: InReplyTo { event_id: original_message.event_id.clone() }, } }; Self { msgtype, relates_to: Some(relates_to) } } /// Create a new message for a thread that is optionally a reply. /// /// Looks for a [`Relation::Thread`] in `previous_message`. If it exists, a message for the same /// thread is created. If it doesn't, a new thread with `previous_message` as the root is /// created. /// /// If `message` is a text, an emote or a notice message, and this is a reply in the thread, it /// is modified to include the rich reply fallback. #[cfg(feature = "unstable-msc3440")] pub fn for_thread( message: MessageType, previous_message: &OriginalRoomMessageEvent, is_reply: ReplyInThread, ) -> Self { let make_reply = |body, formatted: Option| { reply::plain_and_formatted_reply_body(body, formatted.map(|f| f.body), previous_message) }; let msgtype = if is_reply == ReplyInThread::Yes { // If this is a real reply, add the rich reply fallback. match message { MessageType::Text(TextMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Text(TextMessageEventContent::html(body, html_body)) } MessageType::Emote(EmoteMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Emote(EmoteMessageEventContent::html(body, html_body)) } MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => { let (body, html_body) = make_reply(body, formatted); MessageType::Notice(NoticeMessageEventContent::html(body, html_body)) } _ => message, } } else { message }; let thread_root = if let Some(Relation::Thread(Thread { event_id, .. })) = &previous_message.content.relates_to { event_id.clone() } else { previous_message.event_id.clone() }; Self { msgtype, relates_to: Some(Relation::Thread(Thread { event_id: thread_root, in_reply_to: InReplyTo { event_id: previous_message.event_id.clone() }, is_falling_back: is_reply == ReplyInThread::No, })), } } /// Returns a reference to the `msgtype` string. /// /// If you want to access the message type-specific data rather than the message type itself, /// use the `msgtype` *field*, not this method. pub fn msgtype(&self) -> &str { self.msgtype.msgtype() } /// Return a reference to the message body. pub fn body(&self) -> &str { self.msgtype.body() } } /// Whether or not to forward a [`Relation::Thread`] when sending a reply. #[cfg(feature = "unstable-msc3440")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum ForwardThread { /// The thread relation in the original message is forwarded if it exists. /// /// This should be set if your client doesn't support threads (see [MSC3440]). /// /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 Yes, /// Create a reply in the main conversation even if the original message is in a thread. /// /// This should be used if you client supports threads and you explicitly want that behavior. No, } /// Whether or not the message is a reply inside a thread. #[cfg(feature = "unstable-msc3440")] #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[allow(clippy::exhaustive_enums)] pub enum ReplyInThread { /// This is a reply. /// /// Create a proper reply _in_ the thread. Yes, /// This is not a reply. /// /// Create a regular message in the thread, with a reply fallback, according to [MSC3440]. /// /// [MSC3440]: https://github.com/matrix-org/matrix-spec-proposals/pull/3440 No, } /// The content that is specific to each message type variant. #[derive(Clone, Debug, Serialize)] #[serde(untagged)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum MessageType { /// An audio message. Audio(AudioMessageEventContent), /// An emote message. Emote(EmoteMessageEventContent), /// A file message. File(FileMessageEventContent), /// An image message. Image(ImageMessageEventContent), /// A location message. Location(LocationMessageEventContent), /// A notice message. Notice(NoticeMessageEventContent), /// A server notice message. ServerNotice(ServerNoticeMessageEventContent), /// A text message. Text(TextMessageEventContent), /// A video message. Video(VideoMessageEventContent), /// A request to initiate a key verification. VerificationRequest(KeyVerificationRequestEventContent), /// A custom message. #[doc(hidden)] _Custom(CustomEventContent), } impl MessageType { /// Creates a new `MessageType`. /// /// The `msgtype` and `body` are required fields as defined by [the `m.room.message` spec](https://spec.matrix.org/v1.2/client-server-api/#mroommessage). /// Additionally it's possible to add arbitrary key/value pairs to the event content for custom /// events through the `data` map. /// /// Prefer to use the public variants of `MessageType` where possible; this constructor is meant /// be used for unsupported message types only and does not allow setting arbitrary data for /// supported ones. /// /// # Errors /// /// Returns an error if the `msgtype` is known and serialization of `data` to the corresponding /// `MessageType` variant fails. pub fn new(msgtype: &str, body: String, data: JsonObject) -> serde_json::Result { fn deserialize_variant( body: String, mut obj: JsonObject, ) -> serde_json::Result { obj.insert("body".into(), body.into()); serde_json::from_value(JsonValue::Object(obj)) } Ok(match msgtype { "m.audio" => Self::Audio(deserialize_variant(body, data)?), "m.emote" => Self::Emote(deserialize_variant(body, data)?), "m.file" => Self::File(deserialize_variant(body, data)?), "m.image" => Self::Image(deserialize_variant(body, data)?), "m.location" => Self::Location(deserialize_variant(body, data)?), "m.notice" => Self::Notice(deserialize_variant(body, data)?), "m.server_notice" => Self::ServerNotice(deserialize_variant(body, data)?), "m.text" => Self::Text(deserialize_variant(body, data)?), "m.video" => Self::Video(deserialize_variant(body, data)?), "m.key.verification.request" => { Self::VerificationRequest(deserialize_variant(body, data)?) } _ => Self::_Custom(CustomEventContent { msgtype: msgtype.to_owned(), body, data }), }) } /// Returns a reference to the `msgtype` string. pub fn msgtype(&self) -> &str { match self { Self::Audio(_) => "m.audio", Self::Emote(_) => "m.emote", Self::File(_) => "m.file", Self::Image(_) => "m.image", Self::Location(_) => "m.location", Self::Notice(_) => "m.notice", Self::ServerNotice(_) => "m.server_notice", Self::Text(_) => "m.text", Self::Video(_) => "m.video", Self::VerificationRequest(_) => "m.key.verification.request", Self::_Custom(c) => &c.msgtype, } } /// Return a reference to the message body. pub fn body(&self) -> &str { match self { MessageType::Audio(m) => &m.body, MessageType::Emote(m) => &m.body, MessageType::File(m) => &m.body, MessageType::Image(m) => &m.body, MessageType::Location(m) => &m.body, MessageType::Notice(m) => &m.body, MessageType::ServerNotice(m) => &m.body, MessageType::Text(m) => &m.body, MessageType::Video(m) => &m.body, MessageType::VerificationRequest(m) => &m.body, MessageType::_Custom(m) => &m.body, } } /// Returns the associated data. /// /// The returned JSON object won't contain the `msgtype` and `body` fields, use /// [`.msgtype()`][Self::msgtype] / [`.body()`](Self::body) to access those. /// /// Prefer to use the public variants of `MessageType` where possible; this method is meant to /// be used for custom message types only. pub fn data(&self) -> Cow<'_, JsonObject> { fn serialize(obj: &T) -> JsonObject { match serde_json::to_value(obj).expect("message type serialization to succeed") { JsonValue::Object(mut obj) => { obj.remove("body"); obj } _ => panic!("all message types must serialize to objects"), } } match self { Self::Audio(d) => Cow::Owned(serialize(d)), Self::Emote(d) => Cow::Owned(serialize(d)), Self::File(d) => Cow::Owned(serialize(d)), Self::Image(d) => Cow::Owned(serialize(d)), Self::Location(d) => Cow::Owned(serialize(d)), Self::Notice(d) => Cow::Owned(serialize(d)), Self::ServerNotice(d) => Cow::Owned(serialize(d)), Self::Text(d) => Cow::Owned(serialize(d)), Self::Video(d) => Cow::Owned(serialize(d)), Self::VerificationRequest(d) => Cow::Owned(serialize(d)), Self::_Custom(c) => Cow::Borrowed(&c.data), } } } impl From for RoomMessageEventContent { fn from(msgtype: MessageType) -> Self { Self::new(msgtype) } } /// Message event relationship. #[derive(Clone, Debug)] #[allow(clippy::manual_non_exhaustive)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub enum Relation { /// An `m.in_reply_to` relation indicating that the event is a reply to another event. Reply { /// Information about another message being replied to. in_reply_to: InReplyTo, }, /// An event that replaces another event. #[cfg(feature = "unstable-msc2676")] Replacement(Replacement), /// An event that belongs to a thread. #[cfg(feature = "unstable-msc3440")] Thread(Thread), #[doc(hidden)] _Custom, } /// Information about the event a "rich reply" is replying to. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct InReplyTo { /// The event being replied to. pub event_id: OwnedEventId, } impl InReplyTo { /// Creates a new `InReplyTo` with the given event ID. pub fn new(event_id: OwnedEventId) -> Self { Self { event_id } } } /// The event this relation belongs to replaces another event. #[derive(Clone, Debug)] #[cfg(feature = "unstable-msc2676")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Replacement { /// The ID of the event being replaced. pub event_id: OwnedEventId, /// New content. pub new_content: Box, } #[cfg(feature = "unstable-msc2676")] impl Replacement { /// Creates a new `Replacement` with the given event ID and new content. pub fn new(event_id: OwnedEventId, new_content: Box) -> Self { Self { event_id, new_content } } } /// The content of a thread relation. #[derive(Clone, Debug, Deserialize, Serialize)] #[cfg(feature = "unstable-msc3440")] #[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] pub struct Thread { /// The ID of the root message in the thread. pub event_id: OwnedEventId, /// A reply relation. /// /// If this event is a reply and belongs to a thread, this points to the message that is being /// replied to, and `is_falling_back` must be set to `false`. /// /// If this event is not a reply, this is used as a fallback mechanism for clients that do not /// support threads. This should point to the latest message-like event in the thread and /// `is_falling_back` must be set to `true`. pub in_reply_to: InReplyTo, /// Whether the `m.in_reply_to` field is a fallback for older clients or a genuine reply in a /// thread. pub is_falling_back: bool, } #[cfg(feature = "unstable-msc3440")] impl Thread { /// Convenience method to create a regular `Thread` with the given event ID and latest /// message-like event ID. pub fn plain(event_id: OwnedEventId, latest_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(latest_event_id), is_falling_back: true } } /// Convenience method to create a reply `Thread` with the given event ID and replied-to event /// ID. pub fn reply(event_id: OwnedEventId, reply_to_event_id: OwnedEventId) -> Self { Self { event_id, in_reply_to: InReplyTo::new(reply_to_event_id), is_falling_back: false } } } /// The format for the formatted representation of a message body. #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] #[derive(Clone, Debug, PartialEq, Eq, StringEnum)] #[non_exhaustive] pub enum MessageFormat { /// HTML. #[ruma_enum(rename = "org.matrix.custom.html")] Html, #[doc(hidden)] _Custom(PrivOwnedStr), } /// Common message event content fields for message types that have separate plain-text and /// formatted representations. #[derive(Clone, Debug, Deserialize, Serialize)] #[allow(clippy::exhaustive_structs)] pub struct FormattedBody { /// The format used in the `formatted_body`. pub format: MessageFormat, /// The formatted version of the `body`. #[serde(rename = "formatted_body")] pub body: String, } impl FormattedBody { /// Creates a new HTML-formatted message body. pub fn html(body: impl Into) -> Self { Self { format: MessageFormat::Html, body: body.into() } } /// 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)) } } /// The payload for a custom message event. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] pub struct CustomEventContent { /// A custom msgtype. msgtype: String, /// The message body. body: String, /// Remaining event content. #[serde(flatten)] data: JsonObject, }