use std::{borrow::Cow, fmt, time::Duration};
use js_int::UInt;
use ruma_macros::EventContent;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::{EncryptedFile, ImageInfo, MediaSource, ThumbnailInfo};
#[cfg(feature = "unstable-msc3246")]
use crate::events::audio::{AudioContent, AudioEventContent};
#[cfg(feature = "unstable-msc3551")]
use crate::events::file::{FileContent, FileContentInfo, FileEventContent};
#[cfg(feature = "unstable-msc3552")]
use crate::events::image::{ImageContent, ImageEventContent, ThumbnailContent};
#[cfg(feature = "unstable-msc3553")]
use crate::events::video::{VideoContent, VideoEventContent};
#[cfg(feature = "unstable-msc3245")]
use crate::events::voice::{VoiceContent, VoiceEventContent};
#[cfg(feature = "unstable-msc1767")]
use crate::events::{
emote::EmoteEventContent,
message::{MessageContent, MessageEventContent},
notice::NoticeEventContent,
};
use crate::{
events::key::verification::VerificationMethod,
serde::{JsonObject, StringEnum},
OwnedDeviceId, OwnedEventId, OwnedMxcUri, OwnedUserId, PrivOwnedStr,
};
#[cfg(feature = "unstable-msc3488")]
use crate::{
events::location::{AssetContent, LocationContent, LocationEventContent},
MilliSecondsSinceUnixEpoch,
};
mod content_serde;
pub mod feedback;
mod relation_serde;
mod reply;
#[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 {
#[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: &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)
}
}
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)
}
}
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)
}
}
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)
}
}
#[cfg(feature = "unstable-msc3440")]
pub fn reply(
message: MessageType,
original_message: &OriginalRoomMessageEvent,
forward_thread: ForwardThread,
) -> Self {
let msgtype = match message {
MessageType::Text(TextMessageEventContent { body, formatted, .. }) => {
let (body, html_body) = reply::plain_and_formatted_reply_body(
body,
formatted.map(|f| f.body),
original_message,
);
MessageType::Text(TextMessageEventContent::html(body, html_body))
}
MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => {
let (body, html_body) = reply::plain_and_formatted_reply_body(
body,
formatted.map(|f| f.body),
original_message,
);
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) }
}
#[cfg(feature = "unstable-msc3440")]
pub fn for_thread(
message: MessageType,
previous_message: &OriginalRoomMessageEvent,
is_reply: ReplyInThread,
) -> Self {
let msgtype = match message {
MessageType::Text(TextMessageEventContent { body, formatted, .. }) => {
let (body, html_body) = reply::plain_and_formatted_reply_body(
body,
formatted.map(|f| f.body),
previous_message,
);
MessageType::Text(TextMessageEventContent::html(body, html_body))
}
MessageType::Notice(NoticeMessageEventContent { body, formatted, .. }) => {
let (body, html_body) = reply::plain_and_formatted_reply_body(
body,
formatted.map(|f| f.body),
previous_message,
);
MessageType::Notice(NoticeMessageEventContent::html(body, html_body))
}
_ => 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,
})),
}
}
pub fn msgtype(&self) -> &str {
self.msgtype.msgtype()
}
pub fn body(&self) -> &str {
self.msgtype.body()
}
}
#[cfg(feature = "unstable-msc3246")]
impl From<AudioEventContent> for RoomMessageEventContent {
fn from(content: AudioEventContent) -> Self {
let AudioEventContent { message, file, audio, relates_to } = content;
Self {
msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_content(
message, file, audio,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<EmoteEventContent> for RoomMessageEventContent {
fn from(content: EmoteEventContent) -> Self {
let EmoteEventContent { message, relates_to, .. } = content;
Self { msgtype: MessageType::Emote(message.into()), relates_to }
}
}
#[cfg(feature = "unstable-msc3551")]
impl From<FileEventContent> for RoomMessageEventContent {
fn from(content: FileEventContent) -> Self {
let FileEventContent { message, file, relates_to } = content;
Self {
msgtype: MessageType::File(FileMessageEventContent::from_extensible_content(
message, file,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc3552")]
impl From<ImageEventContent> for RoomMessageEventContent {
fn from(content: ImageEventContent) -> Self {
let ImageEventContent { message, file, image, thumbnail, caption, relates_to } = content;
Self {
msgtype: MessageType::Image(ImageMessageEventContent::from_extensible_content(
message, file, image, thumbnail, caption,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc3488")]
impl From<LocationEventContent> for RoomMessageEventContent {
fn from(content: LocationEventContent) -> Self {
let LocationEventContent { message, location, asset, ts, relates_to } = content;
Self {
msgtype: MessageType::Location(LocationMessageEventContent::from_extensible_content(
message, location, asset, ts,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<MessageEventContent> for RoomMessageEventContent {
fn from(content: MessageEventContent) -> Self {
let MessageEventContent { message, relates_to, .. } = content;
Self { msgtype: MessageType::Text(message.into()), relates_to }
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<NoticeEventContent> for RoomMessageEventContent {
fn from(content: NoticeEventContent) -> Self {
let NoticeEventContent { message, relates_to, .. } = content;
Self { msgtype: MessageType::Notice(message.into()), relates_to }
}
}
#[cfg(feature = "unstable-msc3553")]
impl From<VideoEventContent> for RoomMessageEventContent {
fn from(content: VideoEventContent) -> Self {
let VideoEventContent { message, file, video, thumbnail, caption, relates_to } = content;
Self {
msgtype: MessageType::Video(VideoMessageEventContent::from_extensible_content(
message, file, video, thumbnail, caption,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc3245")]
impl From<VoiceEventContent> for RoomMessageEventContent {
fn from(content: VoiceEventContent) -> Self {
let VoiceEventContent { message, file, audio, voice, relates_to } = content;
Self {
msgtype: MessageType::Audio(AudioMessageEventContent::from_extensible_voice_content(
message, file, audio, voice,
)),
relates_to,
}
}
}
#[cfg(feature = "unstable-msc3440")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum ForwardThread {
Yes,
No,
}
#[cfg(feature = "unstable-msc3440")]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[allow(clippy::exhaustive_enums)]
pub enum ReplyInThread {
Yes,
No,
}
#[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),
#[cfg(feature = "unstable-msc3440")]
Thread(Thread),
#[doc(hidden)]
_Custom,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct InReplyTo {
pub event_id: OwnedEventId,
}
impl InReplyTo {
pub fn new(event_id: OwnedEventId) -> 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: OwnedEventId,
pub new_content: Box<RoomMessageEventContent>,
}
#[cfg(feature = "unstable-msc2676")]
impl Replacement {
pub fn new(event_id: OwnedEventId, new_content: Box<RoomMessageEventContent>) -> Self {
Self { event_id, new_content }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg(feature = "unstable-msc3440")]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Thread {
pub event_id: OwnedEventId,
pub in_reply_to: InReplyTo,
pub is_falling_back: bool,
}
#[cfg(feature = "unstable-msc3440")]
impl Thread {
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 }
}
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 }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.audio")]
#[cfg_attr(
feature = "unstable-msc3246",
serde(from = "content_serde::AudioMessageEventContentDeHelper")
)]
pub struct AudioMessageEventContent {
pub body: String,
#[serde(flatten)]
pub source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<AudioInfo>>,
#[cfg(feature = "unstable-msc3246")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[cfg(feature = "unstable-msc3246")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
#[cfg(feature = "unstable-msc3246")]
#[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")]
pub audio: Option<AudioContent>,
#[cfg(feature = "unstable-msc3245")]
#[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")]
pub voice: Option<VoiceContent>,
}
impl AudioMessageEventContent {
pub fn plain(body: String, url: OwnedMxcUri, info: Option<Box<AudioInfo>>) -> Self {
Self {
#[cfg(feature = "unstable-msc3246")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3246")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
#[cfg(feature = "unstable-msc3246")]
audio: Some(info.as_deref().map_or_else(AudioContent::default, Into::into)),
#[cfg(feature = "unstable-msc3245")]
voice: None,
body,
source: MediaSource::Plain(url),
info,
}
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self {
#[cfg(feature = "unstable-msc3246")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3246")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
#[cfg(feature = "unstable-msc3246")]
audio: Some(AudioContent::default()),
#[cfg(feature = "unstable-msc3245")]
voice: None,
body,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
#[cfg(feature = "unstable-msc3246")]
pub fn from_extensible_content(
message: MessageContent,
file: FileContent,
audio: AudioContent,
) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let source = (&file).into();
let info = AudioInfo::from_extensible_content(file.info.as_deref(), &audio).map(Box::new);
Self {
message: Some(message),
file: Some(file),
audio: Some(audio),
#[cfg(feature = "unstable-msc3245")]
voice: None,
body,
source,
info,
}
}
#[cfg(feature = "unstable-msc3245")]
pub fn from_extensible_voice_content(
message: MessageContent,
file: FileContent,
audio: AudioContent,
voice: VoiceContent,
) -> Self {
let mut content = Self::from_extensible_content(message, file, audio);
content.voice = Some(voice);
content
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct AudioInfo {
#[serde(
with = "crate::serde::duration::opt_ms",
default,
skip_serializing_if = "Option::is_none"
)]
pub duration: Option<Duration>,
#[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()
}
#[cfg(feature = "unstable-msc3246")]
pub fn from_extensible_content(
file_info: Option<&FileContentInfo>,
audio: &AudioContent,
) -> Option<Self> {
if file_info.is_none() && audio.is_empty() {
None
} else {
let (mimetype, size) = file_info
.map(|info| (info.mimetype.to_owned(), info.size.to_owned()))
.unwrap_or_default();
let AudioContent { duration, .. } = audio;
Some(Self { duration: duration.to_owned(), mimetype, size })
}
}
}
#[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>,
#[cfg(feature = "unstable-msc1767")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
}
impl EmoteMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::plain(body.clone())),
body,
formatted: None,
}
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
let body = body.into();
let html_body = html_body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::html(body.clone(), html_body.clone())),
body,
formatted: Some(FormattedBody::html(html_body)),
}
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<MessageContent> for EmoteMessageEventContent {
fn from(message: MessageContent) -> Self {
let body = if let Some(body) = message.find_plain() { body } else { &message[0].body };
let formatted = message.find_html().map(FormattedBody::html);
Self { body: body.to_owned(), formatted, message: Some(message) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.file")]
#[cfg_attr(
feature = "unstable-msc3551",
serde(from = "content_serde::FileMessageEventContentDeHelper")
)]
pub struct FileMessageEventContent {
pub body: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(flatten)]
pub source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<FileInfo>>,
#[cfg(feature = "unstable-msc3551")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[cfg(feature = "unstable-msc3551")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
}
impl FileMessageEventContent {
pub fn plain(body: String, url: OwnedMxcUri, info: Option<Box<FileInfo>>) -> Self {
Self {
#[cfg(feature = "unstable-msc3551")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3551")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
body,
filename: None,
source: MediaSource::Plain(url),
info,
}
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self {
#[cfg(feature = "unstable-msc3551")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3551")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
body,
filename: None,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
#[cfg(feature = "unstable-msc3551")]
pub fn from_extensible_content(message: MessageContent, file: FileContent) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let filename = file.info.as_deref().and_then(|info| info.name.clone());
let info = file.info.as_deref().map(|info| Box::new(info.into()));
let source = (&file).into();
Self { message: Some(message), file: Some(file), body, filename, source, info }
}
}
#[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(
flatten,
with = "super::thumbnail_source_serde",
skip_serializing_if = "Option::is_none"
)]
pub thumbnail_source: Option<MediaSource>,
}
impl FileInfo {
pub fn new() -> Self {
Self::default()
}
}
#[cfg(feature = "unstable-msc3551")]
impl From<&FileContentInfo> for FileInfo {
fn from(info: &FileContentInfo) -> Self {
let FileContentInfo { mimetype, size, .. } = info;
Self { mimetype: mimetype.to_owned(), size: size.to_owned(), ..Default::default() }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.image")]
#[cfg_attr(
feature = "unstable-msc3552",
serde(from = "content_serde::ImageMessageEventContentDeHelper")
)]
pub struct ImageMessageEventContent {
pub body: String,
#[serde(flatten)]
pub source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<ImageInfo>>,
#[cfg(feature = "unstable-msc3552")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[cfg(feature = "unstable-msc3552")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
#[cfg(feature = "unstable-msc3552")]
#[serde(rename = "org.matrix.msc1767.image", skip_serializing_if = "Option::is_none")]
pub image: Option<Box<ImageContent>>,
#[cfg(feature = "unstable-msc3552")]
#[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<Vec<ThumbnailContent>>,
#[cfg(feature = "unstable-msc3552")]
#[serde(
rename = "org.matrix.msc1767.caption",
with = "crate::events::message::content_serde::as_vec",
default,
skip_serializing_if = "Option::is_none"
)]
pub caption: Option<MessageContent>,
}
impl ImageMessageEventContent {
pub fn plain(body: String, url: OwnedMxcUri, info: Option<Box<ImageInfo>>) -> Self {
Self {
#[cfg(feature = "unstable-msc3552")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3552")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
#[cfg(feature = "unstable-msc3552")]
image: Some(Box::new(info.as_deref().map_or_else(ImageContent::default, Into::into))),
#[cfg(feature = "unstable-msc3552")]
thumbnail: info
.as_deref()
.and_then(|info| {
ThumbnailContent::from_room_message_content(
info.thumbnail_source.as_ref(),
info.thumbnail_info.as_deref(),
)
})
.map(|thumbnail| vec![thumbnail]),
#[cfg(feature = "unstable-msc3552")]
caption: None,
body,
source: MediaSource::Plain(url),
info,
}
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self {
#[cfg(feature = "unstable-msc3552")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3552")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
#[cfg(feature = "unstable-msc3552")]
image: Some(Box::new(ImageContent::default())),
#[cfg(feature = "unstable-msc3552")]
thumbnail: None,
#[cfg(feature = "unstable-msc3552")]
caption: None,
body,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
#[cfg(feature = "unstable-msc3552")]
pub fn from_extensible_content(
message: MessageContent,
file: FileContent,
image: Box<ImageContent>,
thumbnail: Vec<ThumbnailContent>,
caption: Option<MessageContent>,
) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let source = (&file).into();
let info = ImageInfo::from_extensible_content(file.info.as_deref(), &image, &thumbnail)
.map(Box::new);
let thumbnail = if thumbnail.is_empty() { None } else { Some(thumbnail) };
Self {
message: Some(message),
file: Some(file),
image: Some(image),
thumbnail,
caption,
body,
source,
info,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.location")]
#[cfg_attr(
feature = "unstable-msc3488",
serde(from = "content_serde::LocationMessageEventContentDeHelper")
)]
pub struct LocationMessageEventContent {
pub body: String,
pub geo_uri: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<LocationInfo>>,
#[cfg(feature = "unstable-msc3488")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[cfg(feature = "unstable-msc3488")]
#[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")]
pub location: Option<LocationContent>,
#[cfg(feature = "unstable-msc3488")]
#[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")]
pub asset: Option<AssetContent>,
#[cfg(feature = "unstable-msc3488")]
#[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")]
pub ts: Option<MilliSecondsSinceUnixEpoch>,
}
impl LocationMessageEventContent {
pub fn new(body: String, geo_uri: String) -> Self {
Self {
#[cfg(feature = "unstable-msc3488")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3488")]
location: Some(LocationContent::new(geo_uri.clone())),
#[cfg(feature = "unstable-msc3488")]
asset: None,
#[cfg(feature = "unstable-msc3488")]
ts: None,
body,
geo_uri,
info: None,
}
}
#[cfg(feature = "unstable-msc3488")]
pub fn from_extensible_content(
message: MessageContent,
location: LocationContent,
asset: AssetContent,
ts: Option<MilliSecondsSinceUnixEpoch>,
) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let geo_uri = location.uri.clone();
Self {
message: Some(message),
location: Some(location),
asset: Some(asset),
ts,
body,
geo_uri,
info: None,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct LocationInfo {
#[serde(
flatten,
with = "super::thumbnail_source_serde",
skip_serializing_if = "Option::is_none"
)]
pub thumbnail_source: Option<MediaSource>,
#[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>,
#[cfg(feature = "unstable-msc1767")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
}
impl NoticeMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::plain(body.clone())),
body,
formatted: None,
}
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
let body = body.into();
let html_body = html_body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::html(body.clone(), html_body.clone())),
body,
formatted: Some(FormattedBody::html(html_body)),
}
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<MessageContent> for NoticeMessageEventContent {
fn from(message: MessageContent) -> Self {
let body = if let Some(body) = message.find_plain() { body } else { &message[0].body };
let formatted = message.find_html().map(FormattedBody::html);
Self { body: body.to_owned(), formatted, message: Some(message) }
}
}
#[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 }
}
}
#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
#[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>,
#[cfg(feature = "unstable-msc1767")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
}
impl TextMessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
let body = body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::plain(body.clone())),
body,
formatted: None,
}
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
let body = body.into();
let html_body = html_body.into();
Self {
#[cfg(feature = "unstable-msc1767")]
message: Some(MessageContent::html(body.clone(), html_body.clone())),
body,
formatted: Some(FormattedBody::html(html_body)),
}
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
if let Some(formatted) = FormattedBody::markdown(&body) {
Self::html(body, formatted.body)
} else {
Self::plain(body)
}
}
}
#[cfg(feature = "unstable-msc1767")]
impl From<MessageContent> for TextMessageEventContent {
fn from(message: MessageContent) -> Self {
let body = if let Some(body) = message.find_plain() { body } else { &message[0].body };
let formatted = message.find_html().map(FormattedBody::html);
Self { body: body.to_owned(), formatted, message: Some(message) }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "msgtype", rename = "m.video")]
#[cfg_attr(
feature = "unstable-msc3553",
serde(from = "content_serde::VideoMessageEventContentDeHelper")
)]
pub struct VideoMessageEventContent {
pub body: String,
#[serde(flatten)]
pub source: MediaSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub info: Option<Box<VideoInfo>>,
#[cfg(feature = "unstable-msc3553")]
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub message: Option<MessageContent>,
#[cfg(feature = "unstable-msc3553")]
#[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")]
pub file: Option<FileContent>,
#[cfg(feature = "unstable-msc3553")]
#[serde(rename = "org.matrix.msc1767.video", skip_serializing_if = "Option::is_none")]
pub video: Option<Box<VideoContent>>,
#[cfg(feature = "unstable-msc3553")]
#[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")]
pub thumbnail: Option<Vec<ThumbnailContent>>,
#[cfg(feature = "unstable-msc3553")]
#[serde(
rename = "org.matrix.msc1767.caption",
with = "crate::events::message::content_serde::as_vec",
default,
skip_serializing_if = "Option::is_none"
)]
pub caption: Option<MessageContent>,
}
impl VideoMessageEventContent {
pub fn plain(body: String, url: OwnedMxcUri, info: Option<Box<VideoInfo>>) -> Self {
Self {
#[cfg(feature = "unstable-msc3553")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3553")]
file: Some(FileContent::plain(
url.clone(),
info.as_deref().map(|info| Box::new(info.into())),
)),
#[cfg(feature = "unstable-msc3553")]
video: Some(Box::new(info.as_deref().map_or_else(VideoContent::default, Into::into))),
#[cfg(feature = "unstable-msc3553")]
thumbnail: info
.as_deref()
.and_then(|info| {
ThumbnailContent::from_room_message_content(
info.thumbnail_source.as_ref(),
info.thumbnail_info.as_deref(),
)
})
.map(|thumbnail| vec![thumbnail]),
#[cfg(feature = "unstable-msc3553")]
caption: None,
body,
source: MediaSource::Plain(url),
info,
}
}
pub fn encrypted(body: String, file: EncryptedFile) -> Self {
Self {
#[cfg(feature = "unstable-msc3553")]
message: Some(MessageContent::plain(body.clone())),
#[cfg(feature = "unstable-msc3553")]
file: Some(FileContent::encrypted(file.url.clone(), (&file).into(), None)),
#[cfg(feature = "unstable-msc3553")]
video: Some(Box::new(VideoContent::default())),
#[cfg(feature = "unstable-msc3553")]
thumbnail: None,
#[cfg(feature = "unstable-msc3553")]
caption: None,
body,
source: MediaSource::Encrypted(Box::new(file)),
info: None,
}
}
#[cfg(feature = "unstable-msc3553")]
pub fn from_extensible_content(
message: MessageContent,
file: FileContent,
video: Box<VideoContent>,
thumbnail: Vec<ThumbnailContent>,
caption: Option<MessageContent>,
) -> Self {
let body = if let Some(body) = message.find_plain() {
body.to_owned()
} else {
message[0].body.clone()
};
let source = (&file).into();
let info = VideoInfo::from_extensible_content(file.info.as_deref(), &video, &thumbnail)
.map(Box::new);
let thumbnail = if thumbnail.is_empty() { None } else { Some(thumbnail) };
Self {
message: Some(message),
file: Some(file),
video: Some(video),
thumbnail,
caption,
body,
source,
info,
}
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct VideoInfo {
#[serde(
with = "crate::serde::duration::opt_ms",
default,
skip_serializing_if = "Option::is_none"
)]
pub duration: Option<Duration>,
#[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(
flatten,
with = "super::thumbnail_source_serde",
skip_serializing_if = "Option::is_none"
)]
pub thumbnail_source: Option<MediaSource>,
#[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()
}
#[cfg(feature = "unstable-msc3553")]
pub fn from_extensible_content(
file_info: Option<&FileContentInfo>,
video: &VideoContent,
thumbnail: &[ThumbnailContent],
) -> Option<Self> {
if file_info.is_none() && video.is_empty() && thumbnail.is_empty() {
None
} else {
let (mimetype, size) = file_info
.map(|info| (info.mimetype.to_owned(), info.size.to_owned()))
.unwrap_or_default();
let VideoContent { duration, height, width } = video.to_owned();
let (thumbnail_source, thumbnail_info) = thumbnail
.get(0)
.map(|thumbnail| {
let source = (&thumbnail.file).into();
let info = ThumbnailInfo::from_extensible_content(
thumbnail.file.info.as_deref(),
thumbnail.image.as_deref(),
)
.map(Box::new);
(Some(source), info)
})
.unwrap_or_default();
Some(Self {
duration,
height,
width,
mimetype,
size,
thumbnail_info,
thumbnail_source,
#[cfg(feature = "unstable-msc2448")]
blurhash: None,
})
}
}
}
#[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: OwnedDeviceId,
pub to: OwnedUserId,
}
impl KeyVerificationRequestEventContent {
pub fn new(
body: String,
methods: Vec<VerificationMethod>,
from_device: OwnedDeviceId,
to: OwnedUserId,
) -> 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,
}