use std::{convert::TryFrom, ops::Deref};
use ruma_macros::EventContent;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub(crate) mod content_serde;
use content_serde::MessageContentSerDeHelper;
use super::room::message::{FormattedBody, MessageFormat, Relation, TextMessageEventContent};
#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[ruma_event(type = "m.message", kind = MessageLike)]
pub struct MessageEventContent {
#[serde(flatten)]
pub message: MessageContent,
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Relation>,
}
impl MessageEventContent {
pub fn plain(body: impl Into<String>) -> Self {
Self { message: MessageContent::plain(body), relates_to: None }
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self { message: MessageContent::html(body, html_body), relates_to: None }
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
Self { message: MessageContent::markdown(body), relates_to: None }
}
pub fn from_text_room_message(
content: TextMessageEventContent,
relates_to: Option<Relation>,
) -> Self {
let TextMessageEventContent { body, formatted, message, .. } = content;
if let Some(message) = message {
Self { message, relates_to }
} else {
Self { message: MessageContent::from_room_message_content(body, formatted), relates_to }
}
}
}
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "MessageContentSerDeHelper")]
pub struct MessageContent(pub(crate) Vec<Text>);
impl MessageContent {
pub fn new(messages: Vec<Text>) -> Option<Self> {
if messages.is_empty() {
None
} else {
Some(Self(messages))
}
}
pub fn plain(body: impl Into<String>) -> Self {
Self(vec![Text::plain(body)])
}
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self(vec![Text::html(html_body), Text::plain(body)])
}
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
let mut message = Vec::with_capacity(2);
if let Some(html_body) = Text::markdown(&body) {
message.push(html_body);
}
message.push(Text::plain(body));
Self(message)
}
pub fn from_room_message_content(body: String, formatted: Option<FormattedBody>) -> Self {
if let Some(FormattedBody { body: html_body, .. }) =
formatted.filter(|formatted| formatted.format == MessageFormat::Html)
{
Self::html(body, html_body)
} else {
Self::plain(body)
}
}
pub fn find_plain(&self) -> Option<&str> {
self.iter()
.find(|content| content.mimetype == "text/plain")
.map(|content| content.body.as_ref())
}
pub fn find_html(&self) -> Option<&str> {
self.iter()
.find(|content| content.mimetype == "text/html")
.map(|content| content.body.as_ref())
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
#[error("MessageContent cannot be empty")]
pub struct EmptyMessageContentError;
impl TryFrom<Vec<Text>> for MessageContent {
type Error = EmptyMessageContentError;
fn try_from(messages: Vec<Text>) -> Result<Self, Self::Error> {
Self::new(messages).ok_or(EmptyMessageContentError)
}
}
impl Deref for MessageContent {
type Target = [Text];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Text {
#[serde(default = "Text::default_mimetype")]
pub mimetype: String,
pub body: String,
#[cfg(feature = "unstable-msc3554")]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
}
impl Text {
pub fn plain(body: impl Into<String>) -> Self {
Self {
mimetype: "text/plain".to_owned(),
body: body.into(),
#[cfg(feature = "unstable-msc3554")]
lang: None,
}
}
pub fn html(body: impl Into<String>) -> Self {
Self {
mimetype: "text/html".to_owned(),
body: body.into(),
#[cfg(feature = "unstable-msc3554")]
lang: None,
}
}
#[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))
}
fn default_mimetype() -> String {
"text/plain".to_owned()
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TryFromExtensibleError {
#[error("missing field `{0}`")]
MissingField(String),
}