diff --git a/crates/ruma-common/src/events/room/message.rs b/crates/ruma-common/src/events/room/message.rs index 15892af4..bb103bc5 100644 --- a/crates/ruma-common/src/events/room/message.rs +++ b/crates/ruma-common/src/events/room/message.rs @@ -2,18 +2,16 @@ //! //! [`m.room.message`]: https://spec.matrix.org/v1.2/client-server-api/#mroommessage -use std::{borrow::Cow, fmt, time::Duration}; +use std::{borrow::Cow, fmt}; -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}; +use crate::events::file::{FileContent, FileEventContent}; #[cfg(feature = "unstable-msc3552")] use crate::events::image::{ImageContent, ImageEventContent, ThumbnailContent}; #[cfg(feature = "unstable-msc3553")] @@ -26,21 +24,41 @@ use crate::events::{ 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, }; +use crate::{ + serde::{JsonObject, StringEnum}, + OwnedEventId, PrivOwnedStr, +}; +mod audio; mod content_serde; +mod emote; pub mod feedback; +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. /// @@ -702,804 +720,6 @@ impl Thread { } } -/// The payload for an audio message. -/// -/// With the `unstable-msc3246` feature, this type contains the transitional format of -/// [`AudioEventContent`] and with the `unstable-msc3245` feature, this type also contains the -/// transitional format of [`VoiceEventContent`]. See the documentation of the [`message`] module -/// for more information. -/// -/// [`message`]: crate::events::message -#[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 { - /// The textual representation of this message. - pub body: String, - - /// The source of the audio clip. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata for the audio clip referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3246")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Extensible-event file content of the message. - /// - /// If present, this should be preferred over the `source` and `info` fields. - #[cfg(feature = "unstable-msc3246")] - #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] - pub file: Option, - - /// Extensible-event audio info of the message. - /// - /// If present, this should be preferred over the `info` field. - #[cfg(feature = "unstable-msc3246")] - #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] - pub audio: Option, - - /// Extensible-event voice flag of the message. - /// - /// If present, this should be represented as a voice message. - #[cfg(feature = "unstable-msc3245")] - #[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")] - pub voice: Option, -} - -impl AudioMessageEventContent { - /// Creates a new non-encrypted `AudioMessageEventContent` with the given body, url and - /// optional extra info. - pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, - } - } - - /// Creates a new encrypted `AudioMessageEventContent` with the given body and encrypted - /// file. - 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, - } - } - - /// Create a new `AudioMessageEventContent` with the given message, file info and audio info. - #[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, - } - } - - /// Create a new `AudioMessageEventContent` with the given message, file info, audio info and - /// voice flag. - #[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 - } -} - -/// Metadata about an audio clip. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct AudioInfo { - /// The duration of the audio in milliseconds. - #[serde( - with = "crate::serde::duration::opt_ms", - default, - skip_serializing_if = "Option::is_none" - )] - pub duration: Option, - - /// The mimetype of the audio, e.g. "audio/aac". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the audio clip in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, -} - -impl AudioInfo { - /// Creates an empty `AudioInfo`. - pub fn new() -> Self { - Self::default() - } - - /// Create an `AudioInfo` from the given file info and audio info. - #[cfg(feature = "unstable-msc3246")] - pub fn from_extensible_content( - file_info: Option<&FileContentInfo>, - audio: &AudioContent, - ) -> Option { - 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 }) - } - } -} - -/// The payload for an emote message. -/// -/// With the `unstable-msc1767` feature, this type contains the transitional format of -/// [`EmoteEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::message -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.emote")] -pub struct EmoteMessageEventContent { - /// The emote action to perform. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, - - /// Extensible-event representation of the message. - /// - /// If present, this should be preferred over the other fields. - #[cfg(feature = "unstable-msc1767")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -impl EmoteMessageEventContent { - /// A convenience constructor to create a plain-text emote. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { - #[cfg(feature = "unstable-msc1767")] - message: Some(MessageContent::plain(body.clone())), - body, - formatted: None, - } - } - - /// A convenience constructor to create an html emote message. - pub fn html(body: impl Into, html_body: impl Into) -> 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)), - } - } - - /// A convenience constructor to create a markdown emote. - /// - /// Returns an html emote message if some markdown formatting was detected, otherwise returns a - /// plain-text emote. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} - -#[cfg(feature = "unstable-msc1767")] -impl From 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) } - } -} - -/// The payload for a file message. -/// -/// With the `unstable-msc3551` feature, this type contains the transitional format of -/// [`FileEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::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 { - /// A human-readable description of the file. - /// - /// This is recommended to be the filename of the original upload. - pub body: String, - - /// The original filename of the uploaded file. - #[serde(skip_serializing_if = "Option::is_none")] - pub filename: Option, - - /// The source of the file. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the file referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3551")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Extensible-event file content of the message. - /// - /// If present, this should be preferred over the `source` and `info` fields. - #[cfg(feature = "unstable-msc3551")] - #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] - pub file: Option, -} - -impl FileMessageEventContent { - /// Creates a new non-encrypted `FileMessageEventContent` with the given body, url and - /// optional extra info. - pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, - } - } - - /// Creates a new encrypted `FileMessageEventContent` with the given body and encrypted - /// file. - 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, - } - } - - /// Create a new `FileMessageEventContent` with the given message and file info. - #[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 } - } -} - -/// Metadata about a file. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct FileInfo { - /// The mimetype of the file, e.g. "application/msword". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the file in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Metadata about the image referred to in `thumbnail_source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, - - /// The source of the thumbnail of the file. - #[serde( - flatten, - with = "super::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, -} - -impl FileInfo { - /// Creates an empty `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() } - } -} - -/// The payload for an image message. -/// -/// With the `unstable-msc3552` feature, this type contains the transitional format of -/// [`ImageEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::message -#[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 { - /// A textual representation of the image. - /// - /// Could be the alt text of the image, the filename of the image, or some kind of content - /// description for accessibility e.g. "image attachment". - pub body: String, - - /// The source of the image. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the image referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3552")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Extensible-event file content of the message. - /// - /// If present, this should be preferred over the `source` and `info` fields. - #[cfg(feature = "unstable-msc3552")] - #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] - pub file: Option, - - /// Extensible-event image info of the message. - /// - /// If present, this should be preferred over the `info` field. - #[cfg(feature = "unstable-msc3552")] - #[serde(rename = "org.matrix.msc1767.image", skip_serializing_if = "Option::is_none")] - pub image: Option>, - - /// Extensible-event thumbnails of the message. - /// - /// If present, this should be preferred over the `info` field. - #[cfg(feature = "unstable-msc3552")] - #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] - pub thumbnail: Option>, - - /// Extensible-event captions of the message. - #[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, -} - -impl ImageMessageEventContent { - /// Creates a new non-encrypted `ImageMessageEventContent` with the given body, url and - /// optional extra info. - pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, - } - } - - /// Creates a new encrypted `ImageMessageEventContent` with the given body and encrypted - /// file. - 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, - } - } - - /// Create a new `ImageMessageEventContent` with the given message, file info, image info, - /// thumbnails and captions. - #[cfg(feature = "unstable-msc3552")] - pub fn from_extensible_content( - message: MessageContent, - file: FileContent, - image: Box, - thumbnail: Vec, - caption: Option, - ) -> 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, - } - } -} - -/// The payload for a location message. -/// -/// With the `unstable-msc3488` feature, this type contains the transitional format of -/// [`LocationEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::message -#[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 { - /// A description of the location e.g. "Big Ben, London, UK", or some kind of content - /// description for accessibility, e.g. "location attachment". - pub body: String, - - /// A geo URI representing the location. - pub geo_uri: String, - - /// Info about the location being represented. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3488")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Extensible-event location info of the message. - /// - /// If present, this should be preferred over the `geo_uri` field. - #[cfg(feature = "unstable-msc3488")] - #[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")] - pub location: Option, - - /// Extensible-event asset this message refers to. - #[cfg(feature = "unstable-msc3488")] - #[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")] - pub asset: Option, - - /// Extensible-event timestamp this message refers to. - #[cfg(feature = "unstable-msc3488")] - #[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")] - pub ts: Option, -} - -impl LocationMessageEventContent { - /// Creates a new `LocationMessageEventContent` with the given body and geo URI. - 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, - } - } - - /// Create a new `LocationMessageEventContent` with the given message, location info, asset and - /// timestamp. - #[cfg(feature = "unstable-msc3488")] - pub fn from_extensible_content( - message: MessageContent, - location: LocationContent, - asset: AssetContent, - ts: Option, - ) -> 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, - } - } -} - -/// Thumbnail info associated with a location. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct LocationInfo { - /// The source of a thumbnail of the location. - #[serde( - flatten, - with = "super::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, - - /// Metadata about the image referred to in `thumbnail_source. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, -} - -impl LocationInfo { - /// Creates an empty `LocationInfo`. - pub fn new() -> Self { - Self::default() - } -} - -/// The payload for a notice message. -/// -/// With the `unstable-msc1767` feature, this type contains the transitional format of -/// [`NoticeEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::message -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.notice")] -pub struct NoticeMessageEventContent { - /// The notice text. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, - - /// Extensible-event representation of the message. - /// - /// If present, this should be preferred over the other fields. - #[cfg(feature = "unstable-msc1767")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -impl NoticeMessageEventContent { - /// A convenience constructor to create a plain text notice. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { - #[cfg(feature = "unstable-msc1767")] - message: Some(MessageContent::plain(body.clone())), - body, - formatted: None, - } - } - - /// A convenience constructor to create an html notice. - pub fn html(body: impl Into, html_body: impl Into) -> 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)), - } - } - - /// A convenience constructor to create a markdown notice. - /// - /// Returns an html notice if some markdown formatting was detected, otherwise returns a plain - /// text notice. - #[cfg(feature = "markdown")] - pub fn markdown(body: impl AsRef + Into) -> Self { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} - -#[cfg(feature = "unstable-msc1767")] -impl From 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) } - } -} - -/// The payload for a server notice 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 { - /// A human-readable description of the notice. - pub body: String, - - /// The type of notice being represented. - pub server_notice_type: ServerNoticeType, - - /// A URI giving a contact method for the server administrator. - /// - /// Required if the notice type is `m.server_notice.usage_limit_reached`. - #[serde(skip_serializing_if = "Option::is_none")] - pub admin_contact: Option, - - /// The kind of usage limit the server has exceeded. - /// - /// Required if the notice type is `m.server_notice.usage_limit_reached`. - #[serde(skip_serializing_if = "Option::is_none")] - pub limit_type: Option, -} - -impl ServerNoticeMessageEventContent { - /// Creates a new `ServerNoticeMessageEventContent` with the given body and notice type. - pub fn new(body: String, server_notice_type: ServerNoticeType) -> Self { - Self { body, server_notice_type, admin_contact: None, limit_type: None } - } -} - -/// Types of server notices. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] -#[non_exhaustive] -pub enum ServerNoticeType { - /// The server has exceeded some limit which requires the server administrator to intervene. - #[ruma_enum(rename = "m.server_notice.usage_limit_reached")] - UsageLimitReached, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - -/// Types of usage limits. -#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] -#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] -#[ruma_enum(rename_all = "snake_case")] -#[non_exhaustive] -pub enum LimitType { - /// The server's number of active users in the last 30 days has exceeded the maximum. - /// - /// New connections are being refused by the server. What defines "active" is left as an - /// implementation detail, however servers are encouraged to treat syncing users as "active". - MonthlyActiveUser, - - #[doc(hidden)] - _Custom(PrivOwnedStr), -} - /// 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)] @@ -1546,362 +766,6 @@ impl FormattedBody { } } -/// The payload for a text message. -/// -/// With the `unstable-msc1767` feature, this type contains the transitional format of -/// [`MessageEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::message -#[derive(Clone, Debug, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -#[serde(tag = "msgtype", rename = "m.text")] -pub struct TextMessageEventContent { - /// The body of the message. - pub body: String, - - /// Formatted form of the message `body`. - #[serde(flatten)] - pub formatted: Option, - - /// Extensible-event representation of the message. - /// - /// If present, this should be preferred over the other fields. - #[cfg(feature = "unstable-msc1767")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, -} - -impl TextMessageEventContent { - /// A convenience constructor to create a plain text message. - pub fn plain(body: impl Into) -> Self { - let body = body.into(); - Self { - #[cfg(feature = "unstable-msc1767")] - message: Some(MessageContent::plain(body.clone())), - body, - formatted: None, - } - } - - /// A convenience constructor to create an HTML message. - pub fn html(body: impl Into, html_body: impl Into) -> 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)), - } - } - - /// 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 { - if let Some(formatted) = FormattedBody::markdown(&body) { - Self::html(body, formatted.body) - } else { - Self::plain(body) - } - } -} - -#[cfg(feature = "unstable-msc1767")] -impl From 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) } - } -} - -/// The payload for a video message. -/// -/// With the `unstable-msc3553` feature, this type contains the transitional format of -/// [`VideoEventContent`]. See the documentation of the [`message`] module for more information. -/// -/// [`message`]: crate::events::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 { - /// A description of the video, e.g. "Gangnam Style", or some kind of content description for - /// accessibility, e.g. "video attachment". - pub body: String, - - /// The source of the video clip. - #[serde(flatten)] - pub source: MediaSource, - - /// Metadata about the video clip referred to in `source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub info: Option>, - - /// Extensible-event text representation of the message. - /// - /// If present, this should be preferred over the `body` field. - #[cfg(feature = "unstable-msc3553")] - #[serde(flatten, skip_serializing_if = "Option::is_none")] - pub message: Option, - - /// Extensible-event file content of the message. - /// - /// If present, this should be preferred over the `source` and `info` fields. - #[cfg(feature = "unstable-msc3553")] - #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] - pub file: Option, - - /// Extensible-event video info of the message. - /// - /// If present, this should be preferred over the `info` field. - #[cfg(feature = "unstable-msc3553")] - #[serde(rename = "org.matrix.msc1767.video", skip_serializing_if = "Option::is_none")] - pub video: Option>, - - /// Extensible-event thumbnails of the message. - /// - /// If present, this should be preferred over the `info` field. - #[cfg(feature = "unstable-msc3553")] - #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] - pub thumbnail: Option>, - - /// Extensible-event captions of the message. - #[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, -} - -impl VideoMessageEventContent { - /// Creates a new non-encrypted `VideoMessageEventContent` with the given body, url and - /// optional extra info. - pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, - } - } - - /// Creates a new encrypted `VideoMessageEventContent` with the given body and encrypted - /// file. - 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, - } - } - - /// Create a new `VideoMessageEventContent` with the given message, file info, video info, - /// thumbnails and captions. - #[cfg(feature = "unstable-msc3553")] - pub fn from_extensible_content( - message: MessageContent, - file: FileContent, - video: Box, - thumbnail: Vec, - caption: Option, - ) -> 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, - } - } -} - -/// Metadata about a video. -#[derive(Clone, Debug, Default, Deserialize, Serialize)] -#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] -pub struct VideoInfo { - /// The duration of the video in milliseconds. - #[serde( - with = "crate::serde::duration::opt_ms", - default, - skip_serializing_if = "Option::is_none" - )] - pub duration: Option, - - /// The height of the video in pixels. - #[serde(rename = "h", skip_serializing_if = "Option::is_none")] - pub height: Option, - - /// The width of the video in pixels. - #[serde(rename = "w", skip_serializing_if = "Option::is_none")] - pub width: Option, - - /// The mimetype of the video, e.g. "video/mp4". - #[serde(skip_serializing_if = "Option::is_none")] - pub mimetype: Option, - - /// The size of the video in bytes. - #[serde(skip_serializing_if = "Option::is_none")] - pub size: Option, - - /// Metadata about the image referred to in `thumbnail_source`. - #[serde(skip_serializing_if = "Option::is_none")] - pub thumbnail_info: Option>, - - /// The source of the thumbnail of the video clip. - #[serde( - flatten, - with = "super::thumbnail_source_serde", - skip_serializing_if = "Option::is_none" - )] - pub thumbnail_source: Option, - - /// The [BlurHash](https://blurha.sh) for this video. - /// - /// This uses the unstable prefix in - /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). - #[cfg(feature = "unstable-msc2448")] - #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")] - pub blurhash: Option, -} - -impl VideoInfo { - /// Creates an empty `VideoInfo`. - pub fn new() -> Self { - Self::default() - } - - /// Create a `VideoInfo` from the given file info, video info and thumbnail. - #[cfg(feature = "unstable-msc3553")] - pub fn from_extensible_content( - file_info: Option<&FileContentInfo>, - video: &VideoContent, - thumbnail: &[ThumbnailContent], - ) -> Option { - 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, - }) - } - } -} - -/// The payload for a key verification request message. -#[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 { - /// A fallback message to alert users that their client does not support the key verification - /// framework. - pub body: String, - - /// The verification methods supported by the sender. - pub methods: Vec, - - /// The device ID which is initiating the request. - pub from_device: OwnedDeviceId, - - /// The user ID which should receive the request. - /// - /// Users should only respond to verification requests if they are named in this field. Users - /// who are not named in this field and who did not send this event should ignore all other - /// events that have a `m.reference` relationship with this event. - pub to: OwnedUserId, -} - -impl KeyVerificationRequestEventContent { - /// Creates a new `KeyVerificationRequestEventContent` with the given body, method, device - /// and user ID. - pub fn new( - body: String, - methods: Vec, - from_device: OwnedDeviceId, - to: OwnedUserId, - ) -> Self { - Self { body, methods, from_device, to } - } -} - /// The payload for a custom message event. #[doc(hidden)] #[derive(Clone, Debug, Deserialize, Serialize)] diff --git a/crates/ruma-common/src/events/room/message/audio.rs b/crates/ruma-common/src/events/room/message/audio.rs new file mode 100644 index 00000000..7008d841 --- /dev/null +++ b/crates/ruma-common/src/events/room/message/audio.rs @@ -0,0 +1,203 @@ +use std::time::Duration; + +use js_int::UInt; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc3245")] +use crate::events::voice::VoiceContent; +#[cfg(feature = "unstable-msc3246")] +use crate::events::{ + audio::AudioContent, + file::{FileContent, FileContentInfo}, + message::MessageContent, +}; +use crate::{ + events::room::{EncryptedFile, MediaSource}, + OwnedMxcUri, +}; + +/// The payload for an audio message. +/// +/// With the `unstable-msc3246` feature, this type contains the transitional format of +/// [`AudioEventContent`] and with the `unstable-msc3245` feature, this type also contains the +/// transitional format of [`VoiceEventContent`]. See the documentation of the [`message`] module +/// for more information. +/// +/// [`AudioEventContent`]: crate::events::audio::AudioEventContent +/// [`VoiceEventContent`]: crate::events::voice::VoiceEventContent +/// [`message`]: crate::events::message +#[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 = "super::content_serde::AudioMessageEventContentDeHelper") +)] +pub struct AudioMessageEventContent { + /// The textual representation of this message. + pub body: String, + + /// The source of the audio clip. + #[serde(flatten)] + pub source: MediaSource, + + /// Metadata for the audio clip referred to in `source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3246")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event file content of the message. + /// + /// If present, this should be preferred over the `source` and `info` fields. + #[cfg(feature = "unstable-msc3246")] + #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] + pub file: Option, + + /// Extensible-event audio info of the message. + /// + /// If present, this should be preferred over the `info` field. + #[cfg(feature = "unstable-msc3246")] + #[serde(rename = "org.matrix.msc1767.audio", skip_serializing_if = "Option::is_none")] + pub audio: Option, + + /// Extensible-event voice flag of the message. + /// + /// If present, this should be represented as a voice message. + #[cfg(feature = "unstable-msc3245")] + #[serde(rename = "org.matrix.msc3245.voice", skip_serializing_if = "Option::is_none")] + pub voice: Option, +} + +impl AudioMessageEventContent { + /// Creates a new non-encrypted `AudioMessageEventContent` with the given body, url and + /// optional extra info. + pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, + } + } + + /// Creates a new encrypted `AudioMessageEventContent` with the given body and encrypted + /// file. + 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, + } + } + + /// Create a new `AudioMessageEventContent` with the given message, file info and audio info. + #[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, + } + } + + /// Create a new `AudioMessageEventContent` with the given message, file info, audio info and + /// voice flag. + #[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 + } +} + +/// Metadata about an audio clip. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct AudioInfo { + /// The duration of the audio in milliseconds. + #[serde( + with = "crate::serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none" + )] + pub duration: Option, + + /// The mimetype of the audio, e.g. "audio/aac". + #[serde(skip_serializing_if = "Option::is_none")] + pub mimetype: Option, + + /// The size of the audio clip in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, +} + +impl AudioInfo { + /// Creates an empty `AudioInfo`. + pub fn new() -> Self { + Self::default() + } + + /// Create an `AudioInfo` from the given file info and audio info. + #[cfg(feature = "unstable-msc3246")] + pub fn from_extensible_content( + file_info: Option<&FileContentInfo>, + audio: &AudioContent, + ) -> Option { + 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 }) + } + } +} diff --git a/crates/ruma-common/src/events/room/message/content_serde.rs b/crates/ruma-common/src/events/room/message/content_serde.rs index d4347c82..7bf64c39 100644 --- a/crates/ruma-common/src/events/room/message/content_serde.rs +++ b/crates/ruma-common/src/events/room/message/content_serde.rs @@ -13,12 +13,16 @@ use super::{ #[cfg(feature = "unstable-msc3246")] use super::{AudioContent, AudioInfo, AudioMessageEventContent}; #[cfg(feature = "unstable-msc3551")] -use super::{FileContent, FileInfo, FileMessageEventContent, MediaSource, MessageContent}; +use super::{FileContent, FileInfo, FileMessageEventContent, MessageContent}; #[cfg(feature = "unstable-msc3552")] -use super::{ImageContent, ImageInfo, ImageMessageEventContent, ThumbnailContent}; +use super::{ImageContent, ImageMessageEventContent, ThumbnailContent}; use super::{MessageType, Relation, RoomMessageEventContent}; #[cfg(feature = "unstable-msc3553")] use super::{VideoContent, VideoInfo, VideoMessageEventContent}; +#[cfg(feature = "unstable-msc3552")] +use crate::events::room::ImageInfo; +#[cfg(feature = "unstable-msc3551")] +use crate::events::room::MediaSource; use crate::serde::from_raw_json_value; impl<'de> Deserialize<'de> for RoomMessageEventContent { diff --git a/crates/ruma-common/src/events/room/message/emote.rs b/crates/ruma-common/src/events/room/message/emote.rs new file mode 100644 index 00000000..392aeded --- /dev/null +++ b/crates/ruma-common/src/events/room/message/emote.rs @@ -0,0 +1,79 @@ +use serde::{Deserialize, Serialize}; + +use super::FormattedBody; +#[cfg(feature = "unstable-msc1767")] +use crate::events::message::MessageContent; + +/// The payload for an emote message. +/// +/// With the `unstable-msc1767` feature, this type contains the transitional format of +/// [`EmoteEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`EmoteEventContent`]: crate::events::emote::EmoteEventContent +/// [`message`]: crate::events::message +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "msgtype", rename = "m.emote")] +pub struct EmoteMessageEventContent { + /// The emote action to perform. + pub body: String, + + /// Formatted form of the message `body`. + #[serde(flatten)] + pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl EmoteMessageEventContent { + /// A convenience constructor to create a plain-text emote. + pub fn plain(body: impl Into) -> Self { + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } + } + + /// A convenience constructor to create an html emote message. + pub fn html(body: impl Into, html_body: impl Into) -> 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)), + } + } + + /// A convenience constructor to create a markdown emote. + /// + /// Returns an html emote message if some markdown formatting was detected, otherwise returns a + /// plain-text emote. + #[cfg(feature = "markdown")] + pub fn markdown(body: impl AsRef + Into) -> Self { + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From 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) } + } +} diff --git a/crates/ruma-common/src/events/room/message/file.rs b/crates/ruma-common/src/events/room/message/file.rs new file mode 100644 index 00000000..f7b28af0 --- /dev/null +++ b/crates/ruma-common/src/events/room/message/file.rs @@ -0,0 +1,149 @@ +use js_int::UInt; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc3551")] +use crate::events::{ + file::{FileContent, FileContentInfo}, + message::MessageContent, +}; +use crate::{ + events::room::{EncryptedFile, MediaSource, ThumbnailInfo}, + OwnedMxcUri, +}; + +/// The payload for a file message. +/// +/// With the `unstable-msc3551` feature, this type contains the transitional format of +/// [`FileEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`FileEventContent`]: crate::events::file::FileEventContent +/// [`message`]: crate::events::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 = "super::content_serde::FileMessageEventContentDeHelper") +)] +pub struct FileMessageEventContent { + /// A human-readable description of the file. + /// + /// This is recommended to be the filename of the original upload. + pub body: String, + + /// The original filename of the uploaded file. + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + + /// The source of the file. + #[serde(flatten)] + pub source: MediaSource, + + /// Metadata about the file referred to in `source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3551")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event file content of the message. + /// + /// If present, this should be preferred over the `source` and `info` fields. + #[cfg(feature = "unstable-msc3551")] + #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] + pub file: Option, +} + +impl FileMessageEventContent { + /// Creates a new non-encrypted `FileMessageEventContent` with the given body, url and + /// optional extra info. + pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, + } + } + + /// Creates a new encrypted `FileMessageEventContent` with the given body and encrypted + /// file. + 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, + } + } + + /// Create a new `FileMessageEventContent` with the given message and file info. + #[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 } + } +} + +/// Metadata about a file. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct FileInfo { + /// The mimetype of the file, e.g. "application/msword". + #[serde(skip_serializing_if = "Option::is_none")] + pub mimetype: Option, + + /// The size of the file in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Metadata about the image referred to in `thumbnail_source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_info: Option>, + + /// The source of the thumbnail of the file. + #[serde( + flatten, + with = "crate::events::room::thumbnail_source_serde", + skip_serializing_if = "Option::is_none" + )] + pub thumbnail_source: Option, +} + +impl FileInfo { + /// Creates an empty `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() } + } +} diff --git a/crates/ruma-common/src/events/room/message/image.rs b/crates/ruma-common/src/events/room/message/image.rs new file mode 100644 index 00000000..e01cba19 --- /dev/null +++ b/crates/ruma-common/src/events/room/message/image.rs @@ -0,0 +1,165 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc3552")] +use crate::events::{ + file::FileContent, + image::{ImageContent, ThumbnailContent}, + message::MessageContent, +}; +use crate::{ + events::room::{EncryptedFile, ImageInfo, MediaSource}, + OwnedMxcUri, +}; + +/// The payload for an image message. +/// +/// With the `unstable-msc3552` feature, this type contains the transitional format of +/// [`ImageEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`ImageEventContent`]: crate::events::image::ImageEventContent +/// [`message`]: crate::events::message +#[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 = "super::content_serde::ImageMessageEventContentDeHelper") +)] +pub struct ImageMessageEventContent { + /// A textual representation of the image. + /// + /// Could be the alt text of the image, the filename of the image, or some kind of content + /// description for accessibility e.g. "image attachment". + pub body: String, + + /// The source of the image. + #[serde(flatten)] + pub source: MediaSource, + + /// Metadata about the image referred to in `source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3552")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event file content of the message. + /// + /// If present, this should be preferred over the `source` and `info` fields. + #[cfg(feature = "unstable-msc3552")] + #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] + pub file: Option, + + /// Extensible-event image info of the message. + /// + /// If present, this should be preferred over the `info` field. + #[cfg(feature = "unstable-msc3552")] + #[serde(rename = "org.matrix.msc1767.image", skip_serializing_if = "Option::is_none")] + pub image: Option>, + + /// Extensible-event thumbnails of the message. + /// + /// If present, this should be preferred over the `info` field. + #[cfg(feature = "unstable-msc3552")] + #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] + pub thumbnail: Option>, + + /// Extensible-event captions of the message. + #[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, +} + +impl ImageMessageEventContent { + /// Creates a new non-encrypted `ImageMessageEventContent` with the given body, url and + /// optional extra info. + pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, + } + } + + /// Creates a new encrypted `ImageMessageEventContent` with the given body and encrypted + /// file. + 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, + } + } + + /// Create a new `ImageMessageEventContent` with the given message, file info, image info, + /// thumbnails and captions. + #[cfg(feature = "unstable-msc3552")] + pub fn from_extensible_content( + message: MessageContent, + file: FileContent, + image: Box, + thumbnail: Vec, + caption: Option, + ) -> 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, + } + } +} diff --git a/crates/ruma-common/src/events/room/message/key_verification_request.rs b/crates/ruma-common/src/events/room/message/key_verification_request.rs new file mode 100644 index 00000000..b2c65aa0 --- /dev/null +++ b/crates/ruma-common/src/events/room/message/key_verification_request.rs @@ -0,0 +1,39 @@ +use serde::{Deserialize, Serialize}; + +use crate::{events::key::verification::VerificationMethod, OwnedDeviceId, OwnedUserId}; + +/// The payload for a key verification request message. +#[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 { + /// A fallback message to alert users that their client does not support the key verification + /// framework. + pub body: String, + + /// The verification methods supported by the sender. + pub methods: Vec, + + /// The device ID which is initiating the request. + pub from_device: OwnedDeviceId, + + /// The user ID which should receive the request. + /// + /// Users should only respond to verification requests if they are named in this field. Users + /// who are not named in this field and who did not send this event should ignore all other + /// events that have a `m.reference` relationship with this event. + pub to: OwnedUserId, +} + +impl KeyVerificationRequestEventContent { + /// Creates a new `KeyVerificationRequestEventContent` with the given body, method, device + /// and user ID. + pub fn new( + body: String, + methods: Vec, + from_device: OwnedDeviceId, + to: OwnedUserId, + ) -> Self { + Self { body, methods, from_device, to } + } +} diff --git a/crates/ruma-common/src/events/room/message/location.rs b/crates/ruma-common/src/events/room/message/location.rs new file mode 100644 index 00000000..507f2790 --- /dev/null +++ b/crates/ruma-common/src/events/room/message/location.rs @@ -0,0 +1,132 @@ +use serde::{Deserialize, Serialize}; + +use crate::events::room::{MediaSource, ThumbnailInfo}; +#[cfg(feature = "unstable-msc3488")] +use crate::{ + events::{ + location::{AssetContent, LocationContent}, + message::MessageContent, + }, + MilliSecondsSinceUnixEpoch, +}; + +/// The payload for a location message. +/// +/// With the `unstable-msc3488` feature, this type contains the transitional format of +/// [`LocationEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`LocationEventContent`]: crate::events::location::LocationEventContent +/// [`message`]: crate::events::message +#[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 = "super::content_serde::LocationMessageEventContentDeHelper") +)] +pub struct LocationMessageEventContent { + /// A description of the location e.g. "Big Ben, London, UK", or some kind of content + /// description for accessibility, e.g. "location attachment". + pub body: String, + + /// A geo URI representing the location. + pub geo_uri: String, + + /// Info about the location being represented. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3488")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event location info of the message. + /// + /// If present, this should be preferred over the `geo_uri` field. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.location", skip_serializing_if = "Option::is_none")] + pub location: Option, + + /// Extensible-event asset this message refers to. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.asset", skip_serializing_if = "Option::is_none")] + pub asset: Option, + + /// Extensible-event timestamp this message refers to. + #[cfg(feature = "unstable-msc3488")] + #[serde(rename = "org.matrix.msc3488.ts", skip_serializing_if = "Option::is_none")] + pub ts: Option, +} + +impl LocationMessageEventContent { + /// Creates a new `LocationMessageEventContent` with the given body and geo URI. + 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, + } + } + + /// Create a new `LocationMessageEventContent` with the given message, location info, asset and + /// timestamp. + #[cfg(feature = "unstable-msc3488")] + pub fn from_extensible_content( + message: MessageContent, + location: LocationContent, + asset: AssetContent, + ts: Option, + ) -> 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, + } + } +} + +/// Thumbnail info associated with a location. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct LocationInfo { + /// The source of a thumbnail of the location. + #[serde( + flatten, + with = "crate::events::room::thumbnail_source_serde", + skip_serializing_if = "Option::is_none" + )] + pub thumbnail_source: Option, + + /// Metadata about the image referred to in `thumbnail_source. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_info: Option>, +} + +impl LocationInfo { + /// Creates an empty `LocationInfo`. + pub fn new() -> Self { + Self::default() + } +} diff --git a/crates/ruma-common/src/events/room/message/notice.rs b/crates/ruma-common/src/events/room/message/notice.rs new file mode 100644 index 00000000..0d708b0b --- /dev/null +++ b/crates/ruma-common/src/events/room/message/notice.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc1767")] +use crate::events::message::MessageContent; + +use super::FormattedBody; + +/// The payload for a notice message. +/// +/// With the `unstable-msc1767` feature, this type contains the transitional format of +/// [`NoticeEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`NoticeEventContent`]: crate::events::notice::NoticeEventContent +/// [`message`]: crate::events::message +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "msgtype", rename = "m.notice")] +pub struct NoticeMessageEventContent { + /// The notice text. + pub body: String, + + /// Formatted form of the message `body`. + #[serde(flatten)] + pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl NoticeMessageEventContent { + /// A convenience constructor to create a plain text notice. + pub fn plain(body: impl Into) -> Self { + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } + } + + /// A convenience constructor to create an html notice. + pub fn html(body: impl Into, html_body: impl Into) -> 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)), + } + } + + /// A convenience constructor to create a markdown notice. + /// + /// Returns an html notice if some markdown formatting was detected, otherwise returns a plain + /// text notice. + #[cfg(feature = "markdown")] + pub fn markdown(body: impl AsRef + Into) -> Self { + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From 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) } + } +} diff --git a/crates/ruma-common/src/events/room/message/server_notice.rs b/crates/ruma-common/src/events/room/message/server_notice.rs new file mode 100644 index 00000000..4588852b --- /dev/null +++ b/crates/ruma-common/src/events/room/message/server_notice.rs @@ -0,0 +1,63 @@ +use serde::{Deserialize, Serialize}; + +use crate::{serde::StringEnum, PrivOwnedStr}; + +/// The payload for a server notice 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 { + /// A human-readable description of the notice. + pub body: String, + + /// The type of notice being represented. + pub server_notice_type: ServerNoticeType, + + /// A URI giving a contact method for the server administrator. + /// + /// Required if the notice type is `m.server_notice.usage_limit_reached`. + #[serde(skip_serializing_if = "Option::is_none")] + pub admin_contact: Option, + + /// The kind of usage limit the server has exceeded. + /// + /// Required if the notice type is `m.server_notice.usage_limit_reached`. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit_type: Option, +} + +impl ServerNoticeMessageEventContent { + /// Creates a new `ServerNoticeMessageEventContent` with the given body and notice type. + pub fn new(body: String, server_notice_type: ServerNoticeType) -> Self { + Self { body, server_notice_type, admin_contact: None, limit_type: None } + } +} + +/// Types of server notices. +#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] +#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] +#[non_exhaustive] +pub enum ServerNoticeType { + /// The server has exceeded some limit which requires the server administrator to intervene. + #[ruma_enum(rename = "m.server_notice.usage_limit_reached")] + UsageLimitReached, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} + +/// Types of usage limits. +#[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))] +#[derive(Clone, Debug, PartialEq, Eq, StringEnum)] +#[ruma_enum(rename_all = "snake_case")] +#[non_exhaustive] +pub enum LimitType { + /// The server's number of active users in the last 30 days has exceeded the maximum. + /// + /// New connections are being refused by the server. What defines "active" is left as an + /// implementation detail, however servers are encouraged to treat syncing users as "active". + MonthlyActiveUser, + + #[doc(hidden)] + _Custom(PrivOwnedStr), +} diff --git a/crates/ruma-common/src/events/room/message/text.rs b/crates/ruma-common/src/events/room/message/text.rs new file mode 100644 index 00000000..87081adb --- /dev/null +++ b/crates/ruma-common/src/events/room/message/text.rs @@ -0,0 +1,80 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc1767")] +use crate::events::message::MessageContent; + +use super::FormattedBody; + +/// The payload for a text message. +/// +/// With the `unstable-msc1767` feature, this type contains the transitional format of +/// [`MessageEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`MessageEventContent`]: crate::events::message::MessageEventContent +/// [`message`]: crate::events::message +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +#[serde(tag = "msgtype", rename = "m.text")] +pub struct TextMessageEventContent { + /// The body of the message. + pub body: String, + + /// Formatted form of the message `body`. + #[serde(flatten)] + pub formatted: Option, + + /// Extensible-event representation of the message. + /// + /// If present, this should be preferred over the other fields. + #[cfg(feature = "unstable-msc1767")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, +} + +impl TextMessageEventContent { + /// A convenience constructor to create a plain text message. + pub fn plain(body: impl Into) -> Self { + let body = body.into(); + Self { + #[cfg(feature = "unstable-msc1767")] + message: Some(MessageContent::plain(body.clone())), + body, + formatted: None, + } + } + + /// A convenience constructor to create an HTML message. + pub fn html(body: impl Into, html_body: impl Into) -> 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)), + } + } + + /// 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 { + if let Some(formatted) = FormattedBody::markdown(&body) { + Self::html(body, formatted.body) + } else { + Self::plain(body) + } + } +} + +#[cfg(feature = "unstable-msc1767")] +impl From 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) } + } +} diff --git a/crates/ruma-common/src/events/room/message/video.rs b/crates/ruma-common/src/events/room/message/video.rs new file mode 100644 index 00000000..c4c0c4fe --- /dev/null +++ b/crates/ruma-common/src/events/room/message/video.rs @@ -0,0 +1,264 @@ +use std::time::Duration; + +use js_int::UInt; +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "unstable-msc3553")] +use crate::events::{ + file::{FileContent, FileContentInfo}, + image::ThumbnailContent, + message::MessageContent, + video::VideoContent, +}; +use crate::{ + events::room::{EncryptedFile, MediaSource, ThumbnailInfo}, + OwnedMxcUri, +}; + +/// The payload for a video message. +/// +/// With the `unstable-msc3553` feature, this type contains the transitional format of +/// [`VideoEventContent`]. See the documentation of the [`message`] module for more information. +/// +/// [`VideoEventContent`]: crate::events::video::VideoEventContent +/// [`message`]: crate::events::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 = "super::content_serde::VideoMessageEventContentDeHelper") +)] +pub struct VideoMessageEventContent { + /// A description of the video, e.g. "Gangnam Style", or some kind of content description for + /// accessibility, e.g. "video attachment". + pub body: String, + + /// The source of the video clip. + #[serde(flatten)] + pub source: MediaSource, + + /// Metadata about the video clip referred to in `source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub info: Option>, + + /// Extensible-event text representation of the message. + /// + /// If present, this should be preferred over the `body` field. + #[cfg(feature = "unstable-msc3553")] + #[serde(flatten, skip_serializing_if = "Option::is_none")] + pub message: Option, + + /// Extensible-event file content of the message. + /// + /// If present, this should be preferred over the `source` and `info` fields. + #[cfg(feature = "unstable-msc3553")] + #[serde(rename = "org.matrix.msc1767.file", skip_serializing_if = "Option::is_none")] + pub file: Option, + + /// Extensible-event video info of the message. + /// + /// If present, this should be preferred over the `info` field. + #[cfg(feature = "unstable-msc3553")] + #[serde(rename = "org.matrix.msc1767.video", skip_serializing_if = "Option::is_none")] + pub video: Option>, + + /// Extensible-event thumbnails of the message. + /// + /// If present, this should be preferred over the `info` field. + #[cfg(feature = "unstable-msc3553")] + #[serde(rename = "org.matrix.msc1767.thumbnail", skip_serializing_if = "Option::is_none")] + pub thumbnail: Option>, + + /// Extensible-event captions of the message. + #[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, +} + +impl VideoMessageEventContent { + /// Creates a new non-encrypted `VideoMessageEventContent` with the given body, url and + /// optional extra info. + pub fn plain(body: String, url: OwnedMxcUri, info: Option>) -> 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, + } + } + + /// Creates a new encrypted `VideoMessageEventContent` with the given body and encrypted + /// file. + 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, + } + } + + /// Create a new `VideoMessageEventContent` with the given message, file info, video info, + /// thumbnails and captions. + #[cfg(feature = "unstable-msc3553")] + pub fn from_extensible_content( + message: MessageContent, + file: FileContent, + video: Box, + thumbnail: Vec, + caption: Option, + ) -> 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, + } + } +} + +/// Metadata about a video. +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct VideoInfo { + /// The duration of the video in milliseconds. + #[serde( + with = "crate::serde::duration::opt_ms", + default, + skip_serializing_if = "Option::is_none" + )] + pub duration: Option, + + /// The height of the video in pixels. + #[serde(rename = "h", skip_serializing_if = "Option::is_none")] + pub height: Option, + + /// The width of the video in pixels. + #[serde(rename = "w", skip_serializing_if = "Option::is_none")] + pub width: Option, + + /// The mimetype of the video, e.g. "video/mp4". + #[serde(skip_serializing_if = "Option::is_none")] + pub mimetype: Option, + + /// The size of the video in bytes. + #[serde(skip_serializing_if = "Option::is_none")] + pub size: Option, + + /// Metadata about the image referred to in `thumbnail_source`. + #[serde(skip_serializing_if = "Option::is_none")] + pub thumbnail_info: Option>, + + /// The source of the thumbnail of the video clip. + #[serde( + flatten, + with = "crate::events::room::thumbnail_source_serde", + skip_serializing_if = "Option::is_none" + )] + pub thumbnail_source: Option, + + /// The [BlurHash](https://blurha.sh) for this video. + /// + /// This uses the unstable prefix in + /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448). + #[cfg(feature = "unstable-msc2448")] + #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")] + pub blurhash: Option, +} + +impl VideoInfo { + /// Creates an empty `VideoInfo`. + pub fn new() -> Self { + Self::default() + } + + /// Create a `VideoInfo` from the given file info, video info and thumbnail. + #[cfg(feature = "unstable-msc3553")] + pub fn from_extensible_content( + file_info: Option<&FileContentInfo>, + video: &VideoContent, + thumbnail: &[ThumbnailContent], + ) -> Option { + 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, + }) + } + } +}