use std::{borrow::Cow, fmt};
use js_int::UInt;
use ruma_events_macros::EventContent;
use ruma_identifiers::{DeviceId, EventId, MxcUri, UserId};
use ruma_serde::{JsonObject, StringEnum};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::{EncryptedFile, ImageInfo, ThumbnailInfo};
use crate::{key::verification::VerificationMethod, PrivOwnedStr};
mod content_serde;
pub mod feedback;
mod relation_serde;
mod reply;
pub use reply::ReplyBaseEvent;
#[derive(Clone, Debug, Serialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.room.message", kind = Message)]
pub struct RoomMessageEventContent {
#[serde(flatten)]
pub msgtype: MessageType,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Relation>,
}
impl RoomMessageEventContent {
pub fn new(msgtype: MessageType) -> Self {
Self { msgtype, relates_to: None }
}
pub fn text_plain(body: impl Into<String>) -> Self {
Self::new(MessageType::Text(TextMessageEventContent::plain(body)))
}
pub fn text_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self::new(MessageType::Text(TextMessageEventContent::html(body, html_body)))
}
#[cfg(feature = "markdown")]
pub fn text_markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self::new(MessageType::Text(TextMessageEventContent::markdown(body)))
}
pub fn notice_plain(body: impl Into<String>) -> Self {
Self::new(MessageType::Notice(NoticeMessageEventContent::plain(body)))
}
pub fn notice_html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self::new(MessageType::Notice(NoticeMessageEventContent::html(body, html_body)))
}
#[cfg(feature = "markdown")]
pub fn notice_markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self::new(MessageType::Notice(NoticeMessageEventContent::markdown(body)))
}
pub fn text_reply_plain(
reply: impl fmt::Display,
original_message: &impl ReplyBaseEvent,
) -> Self {
let quoted = reply::get_plain_quote_fallback(original_message);
let body = format!("{}\n\n{}", quoted, reply);
Self {
relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: original_message.event_id().to_owned() },
}),
..Self::text_plain(body)
}
}
pub fn text_reply_html(
reply: impl fmt::Display,
html_reply: impl fmt::Display,
original_message: &RoomMessageEvent,
) -> Self {
let quoted = reply::get_plain_quote_fallback(original_message);
let quoted_html = reply::get_html_quote_fallback(original_message);
let body = format!("{}\n\n{}", quoted, reply);
let html_body = format!("{}\n\n{}", quoted_html, html_reply);
Self {
relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: original_message.event_id.clone() },
}),
..Self::text_html(body, html_body)
}
}
pub fn notice_reply_plain(
reply: impl fmt::Display,
original_message: &impl ReplyBaseEvent,
) -> Self {
let quoted = reply::get_plain_quote_fallback(original_message);
let body = format!("{}\n\n{}", quoted, reply);
Self {
relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: original_message.event_id().to_owned() },
}),
..Self::notice_plain(body)
}
}
pub fn notice_reply_html(
reply: impl fmt::Display,
html_reply: impl fmt::Display,
original_message: &RoomMessageEvent,
) -> Self {
let quoted = reply::get_plain_quote_fallback(original_message);
let quoted_html = reply::get_html_quote_fallback(original_message);
let body = format!("{}\n\n{}", quoted, reply);
let html_body = format!("{}\n\n{}", quoted_html, html_reply);
Self {
relates_to: Some(Relation::Reply {
in_reply_to: InReplyTo { event_id: original_message.event_id.clone() },
}),
..Self::notice_html(body, html_body)
}
}
pub fn msgtype(&self) -> &str {
self.msgtype.msgtype()
}
pub fn body(&self) -> &str {
self.msgtype.body()
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(untagged)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum MessageType {
Audio(AudioMessageEventContent),
Emote(EmoteMessageEventContent),
File(FileMessageEventContent),
Image(ImageMessageEventContent),
Location(LocationMessageEventContent),
Notice(NoticeMessageEventContent),
ServerNotice(ServerNoticeMessageEventContent),
Text(TextMessageEventContent),
Video(VideoMessageEventContent),
VerificationRequest(KeyVerificationRequestEventContent),
#[doc(hidden)]
_Custom(CustomEventContent),
}
impl MessageType {
pub fn new(msgtype: &str, body: String, data: JsonObject) -> serde_json::Result<Self> {
fn deserialize_variant<T: DeserializeOwned>(
body: String,
mut obj: JsonObject,
) -> serde_json::Result<T> {
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 }),
})
}
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,
}
}
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,
}
}
pub fn data(&self) -> Cow<'_, JsonObject> {
fn serialize<T: 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<MessageType> for RoomMessageEventContent {
fn from(msgtype: MessageType) -> Self {
Self::new(msgtype)
}
}
#[derive(Clone, Debug)]
#[allow(clippy::manual_non_exhaustive)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub enum Relation {
Reply {
in_reply_to: InReplyTo,
},
#[cfg(feature = "unstable-msc2676")]
Replacement(Replacement),
#[doc(hidden)]
_Custom,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct InReplyTo {
pub event_id: Box<EventId>,
}
impl InReplyTo {
pub fn new(event_id: Box<EventId>) -> Self {
Self { event_id }
}
}
#[derive(Clone, Debug)]
#[cfg(feature = "unstable-msc2676")]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Replacement {
pub event_id: Box<EventId>,
pub new_content: Box<RoomMessageEventContent>,
}
#[cfg(feature = "unstable-msc2676")]
impl Replacement {
pub fn new(event_id: Box<EventId>, new_content: Box<RoomMessageEventContent>) -> Self {
Self { event_id, new_content }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.audio")]
pub struct AudioMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<AudioInfo>>,
}
impl AudioMessageEventContent {
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<AudioInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct AudioInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
}
impl AudioInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.emote")]
pub struct EmoteMessageEventContent {
pub body: String,
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
}
impl EmoteMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
Self { body: body.into(), formatted: None }
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) }
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.file")]
pub struct FileMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<FileInfo>>,
}
impl FileMessageEventContent {
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<FileInfo>>) -> Self {
Self { body, filename: None, url: Some(url), info, file: None }
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, filename: None, url: None, info: None, file: Some(Box::new(file)) }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct FileInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
}
impl FileInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.image")]
pub struct ImageMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<ImageInfo>>,
}
impl ImageMessageEventContent {
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<ImageInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.location")]
pub struct LocationMessageEventContent {
pub body: String,
pub geo_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<LocationInfo>>,
}
impl LocationMessageEventContent {
pub fn new(body: String, geo_uri: String) -> Self {
Self { body, geo_uri, info: None }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct LocationInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
}
impl LocationInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.notice")]
pub struct NoticeMessageEventContent {
pub body: String,
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
}
impl NoticeMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
Self { body: body.into(), formatted: None }
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) }
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.server_notice")]
pub struct ServerNoticeMessageEventContent {
pub body: String,
pub server_notice_type: ServerNoticeType,
#[serde(skip_serializing_if = "Option::is_none")]
pub admin_contact: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit_type: Option<LimitType>,
}
impl ServerNoticeMessageEventContent {
pub fn new(body: String, server_notice_type: ServerNoticeType) -> Self {
Self { body, server_notice_type, admin_contact: None, limit_type: None }
}
}
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]
#[non_exhaustive]
pub enum ServerNoticeType {
#[ruma_enum(rename = "m.server_notice.usage_limit_reached")]
UsageLimitReached,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl ServerNoticeType {
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]
#[ruma_enum(rename_all = "snake_case")]
#[non_exhaustive]
pub enum LimitType {
MonthlyActiveUser,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl LimitType {
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone, Debug, PartialEq, Eq, StringEnum)]
#[non_exhaustive]
pub enum MessageFormat {
#[ruma_enum(rename = "org.matrix.custom.html")]
Html,
#[doc(hidden)]
_Custom(PrivOwnedStr),
}
impl MessageFormat {
pub fn as_str(&self) -> &str {
self.as_ref()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[allow(clippy::exhaustive_structs)]
pub struct FormattedBody {
pub format: MessageFormat,
#[serde(rename = "formatted_body")]
pub body: String,
}
impl FormattedBody {
pub fn html(body: impl Into<String>) -> Self {
Self { format: MessageFormat::Html, body: body.into() }
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str>) -> Option<Self> {
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!("<p>{}</p>\n", body)).then(|| Self::html(html_body))
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.text")]
pub struct TextMessageEventContent {
pub body: String,
#[serde(flatten)]
pub formatted: Option<FormattedBody>,
}
impl TextMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
Self { body: body.into(), formatted: None }
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self { formatted: Some(FormattedBody::html(html_body)), ..Self::plain(body) }
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self { formatted: FormattedBody::markdown(&body), ..Self::plain(body) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.video")]
pub struct VideoMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<Box<EncryptedFile>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<VideoInfo>>,
}
impl VideoMessageEventContent {
pub fn plain(body: String, url: Box<MxcUri>, info: Option<Box<VideoInfo>>) -> Self {
Self { body, url: Some(url), info, file: None }
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self { body, url: None, info: None, file: Some(Box::new(file)) }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct VideoInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration: Option<UInt>,
#[serde(rename = "h", skip_serializing_if = "Option::is_none")]
pub height: Option<UInt>,
#[serde(rename = "w", skip_serializing_if = "Option::is_none")]
pub width: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mimetype: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_info: Option<Box<ThumbnailInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_url: Option<Box<MxcUri>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub thumbnail_file: Option<Box<EncryptedFile>>,
#[cfg(feature = "unstable-msc2448")]
#[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
pub blurhash: Option<String>,
}
impl VideoInfo {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.key.verification.request")]
pub struct KeyVerificationRequestEventContent {
pub body: String,
pub methods: Vec<VerificationMethod>,
pub from_device: Box<DeviceId>,
pub to: Box<UserId>,
}
impl KeyVerificationRequestEventContent {
pub fn new(
body: String,
methods: Vec<VerificationMethod>,
from_device: Box<DeviceId>,
to: Box<UserId>,
) -> Self {
Self { body, methods, from_device, to }
}
}
#[doc(hidden)]
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CustomEventContent {
msgtype: String,
body: String,
#[serde(flatten)]
data: JsonObject,
}
#[cfg(test)]
mod tests {
use matches::assert_matches;
use ruma_identifiers::event_id;
use serde_json::{from_value as from_json_value, json};
use super::{InReplyTo, MessageType, Relation, RoomMessageEventContent};
#[test]
fn deserialize_reply() {
let ev_id = event_id!("$1598361704261elfgc:localhost");
let json = json!({
"msgtype": "m.text",
"body": "<text msg>",
"m.relates_to": {
"m.in_reply_to": {
"event_id": ev_id,
},
},
});
assert_matches!(
from_json_value::<RoomMessageEventContent>(json).unwrap(),
RoomMessageEventContent {
msgtype: MessageType::Text(_),
relates_to: Some(Relation::Reply { in_reply_to: InReplyTo { event_id } }),
} if event_id == ev_id
);
}
}