1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
//! Types for extensible text message events ([MSC1767]).
//!
//! # Extensible events
//!
//! MSCs [1767] (Text, Emote and Notice), [3551] (Files), [3552] (Images and Stickers), [3553]
//! (Videos), [3246] (Audio), and [3488] (Location) introduce new primary types called extensible
//! events. These types are meant to replace the `m.room.message` primary type and its `msgtype`s.
//! Other MSCs introduce new types with an `m.room.message` fallback, like [MSC3245] (Voice
//! Messages), and types that only have an extensible events format, like [MSC3381] (Polls).
//!
//! # Transition Period
//!
//! MSC1767 defines a transition period that will start after the extensible events are released in
//! a Matrix version. It should last approximately one year, but the end of that period will be
//! formalized in a new Matrix version.
//!
//! The new primary types should not be sent over the Matrix network before the end of the
//! transition period. Instead, transitional `m.room.message` events should be sent. These
//! transitional events include the content of the now legacy `m.room.message` event and the content
//! of the new extensible event types in a single event.
//!
//! # How to use them
//!
//! First, you can enable the `unstable-extensible-events` feature from the `ruma` crate, that
//! will enable all the MSCs for the extensible events that correspond to the legacy `msgtype`s
//! (1767, 3246, 3488, 3551, 3552, 3553). It is also possible to enable only the MSCs you want with
//! the `unstable-mscXXXX` features (where `XXXX` is the number of the MSC).
//!
//! The recommended way to send transitional extensible events while they are unstable and during
//! the transition period is to build one of the new primary types and then to convert it to a
//! [`RoomMessageEventContent`] by using `.into()` or `RoomMessageEventContent::from()`. The
//! provided constructors will copy the relevant data in the legacy fields.
//!
//! For incoming events, a `RoomMessageEventContent` can be converted to an extensible event with
//! the relevant `from_*_room_message` method on the primary type. This conversion will work even
//! with legacy `m.room.message` events that don't have extensible events content.
//!
//! It is also possible to enable extensible events support and continue using
//! `RoomMessageEventContent`'s constructors. The data will be duplicated in both the legacy and
//! extensible events fields.
//!
//! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
//! [1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
//! [3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551
//! [3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552
//! [3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553
//! [3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246
//! [3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488
//! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245
//! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
//! [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent
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};
/// The payload for an extensible text message.
///
/// This is the new primary type introduced in [MSC1767] and should not be sent before the end of
/// the transition period. See the documentation of the [`message`] module for more information.
///
/// `MessageEventContent` can be converted to a [`RoomMessageEventContent`] with a
/// [`MessageType::Text`]. You can convert it back with
/// [`MessageEventContent::from_text_room_message()`].
///
/// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
/// [`message`]: super::message
/// [`RoomMessageEventContent`]: super::room::message::RoomMessageEventContent
/// [`MessageType::Text`]: super::room::message::MessageType::Text
#[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 {
/// The message's text content.
#[serde(flatten)]
pub message: MessageContent,
/// Information about related messages.
#[serde(flatten, skip_serializing_if = "Option::is_none")]
pub relates_to: Option<Relation>,
}
impl MessageEventContent {
/// A convenience constructor to create a plain text message.
pub fn plain(body: impl Into<String>) -> Self {
Self { message: MessageContent::plain(body), relates_to: None }
}
/// A convenience constructor to create an HTML message.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self { message: MessageContent::html(body, html_body), relates_to: None }
}
/// 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<str> + Into<String>) -> Self {
Self { message: MessageContent::markdown(body), relates_to: None }
}
/// Create a new `MessageEventContent` from the given `TextMessageEventContent` and optional
/// relation.
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 }
}
}
}
/// Text message content.
///
/// A `MessageContent` must contain at least one message to be used as a fallback text
/// representation.
#[derive(Clone, Debug, Deserialize)]
#[serde(try_from = "MessageContentSerDeHelper")]
pub struct MessageContent(pub(crate) Vec<Text>);
impl MessageContent {
/// Create a `MessageContent` from an array of messages.
///
/// Returns `None` if the array is empty.
pub fn new(messages: Vec<Text>) -> Option<Self> {
if messages.is_empty() {
None
} else {
Some(Self(messages))
}
}
/// A convenience constructor to create a plain text message.
pub fn plain(body: impl Into<String>) -> Self {
Self(vec![Text::plain(body)])
}
/// A convenience constructor to create an HTML message.
pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
Self(vec![Text::html(html_body), Text::plain(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<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)
}
/// Create a new `MessageContent` from the given body and optional formatted body.
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)
}
}
/// Get the plain text representation of this message.
pub fn find_plain(&self) -> Option<&str> {
self.iter()
.find(|content| content.mimetype == "text/plain")
.map(|content| content.body.as_ref())
}
/// Get the HTML representation of this message.
pub fn find_html(&self) -> Option<&str> {
self.iter()
.find(|content| content.mimetype == "text/html")
.map(|content| content.body.as_ref())
}
}
/// The error type returned when trying to construct an empty `MessageContent`.
#[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
}
}
/// Text message content.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct Text {
/// The mime type of the `body`.
#[serde(default = "Text::default_mimetype")]
pub mimetype: String,
/// The text content.
pub body: String,
/// The language of the text ([MSC3554]).
///
/// This must be a valid language code according to [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt).
///
/// [MSC3554]: https://github.com/matrix-org/matrix-spec-proposals/pull/3554
#[cfg(feature = "unstable-msc3554")]
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
}
impl Text {
/// Creates a new plain text message body.
pub fn plain(body: impl Into<String>) -> Self {
Self {
mimetype: "text/plain".to_owned(),
body: body.into(),
#[cfg(feature = "unstable-msc3554")]
lang: None,
}
}
/// Creates a new HTML-formatted message body.
pub fn html(body: impl Into<String>) -> Self {
Self {
mimetype: "text/html".to_owned(),
body: body.into(),
#[cfg(feature = "unstable-msc3554")]
lang: None,
}
}
/// Creates a new HTML-formatted message body by parsing the Markdown in `body`.
///
/// Returns `None` if no Markdown formatting was found.
#[cfg(feature = "markdown")]
pub fn markdown(body: impl AsRef<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()
}
}
/// The error type returned when a conversion to an extensible event type fails.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum TryFromExtensibleError {
/// A field is missing.
#[error("missing field `{0}`")]
MissingField(String),
}
