ruma_events/message.rs
1//! Types for extensible text message events ([MSC1767]).
2//!
3//! # Extensible events
4//!
5//! [MSC1767] defines a new structure for events that is made of two parts: a type and zero or more
6//! reusable content blocks.
7//!
8//! This allows to construct new event types from a list of known content blocks that allows in turn
9//! clients to be able to render unknown event types by using the known content blocks as a
10//! fallback. When a new type is defined, all the content blocks it can or must contain are defined
11//! too.
12//!
13//! There are also some content blocks called "mixins" that can apply to any event when they are
14//! defined.
15//!
16//! # MSCs
17//!
18//! This is a list of MSCs defining the extensible events and deprecating the corresponding legacy
19//! types. Note that "primary type" means the `type` field at the root of the event and "message
20//! type" means the `msgtype` field in the content of the `m.room.message` primary type.
21//!
22//! - [MSC1767][]: Text messages, where the `m.message` primary type replaces the `m.text` message
23//! type.
24//! - [MSC3954][]: Emotes, where the `m.emote` primary type replaces the `m.emote` message type.
25//! - [MSC3955][]: Automated events, where the `m.automated` mixin replaces the `m.notice` message
26//! type.
27//! - [MSC3956][]: Encrypted events, where the `m.encrypted` primary type replaces the
28//! `m.room.encrypted` primary type.
29//! - [MSC3551][]: Files, where the `m.file` primary type replaces the `m.file` message type.
30//! - [MSC3552][]: Images and Stickers, where the `m.image` primary type replaces the `m.image`
31//! message type and the `m.sticker` primary type.
32//! - [MSC3553][]: Videos, where the `m.video` primary type replaces the `m.video` message type.
33//! - [MSC3927][]: Audio, where the `m.audio` primary type replaces the `m.audio` message type.
34//! - [MSC3488][]: Location, where the `m.location` primary type replaces the `m.location` message
35//! type.
36//!
37//! There are also the following MSCs that introduce new features with extensible events:
38//!
39//! - [MSC3245][]: Voice Messages.
40//! - [MSC3246][]: Audio Waveform.
41//! - [MSC3381][]: Polls.
42//!
43//! # How to use them in Matrix
44//!
45//! The extensible events types are meant to be used separately than the legacy types. As such,
46//! their use is reserved for room versions that support it.
47//!
48//! Currently no stable room version supports extensible events so they can only be sent with
49//! unstable room versions that support them.
50//!
51//! An exception is made for some new extensible events types that don't have a legacy type. They
52//! can be used with stable room versions without support for extensible types, but they might be
53//! ignored by clients that have no support for extensible events. The types that support this must
54//! advertise it in their MSC.
55//!
56//! Note that if a room version supports extensible events, it doesn't support the legacy types
57//! anymore and those should be ignored. There is not yet a definition of the deprecated legacy
58//! types in extensible events rooms.
59//!
60//! # How to use them in Ruma
61//!
62//! First, you can enable the `unstable-extensible-events` feature from the `ruma` crate, that
63//! will enable all the MSCs for the extensible events that correspond to the legacy types. It
64//! is also possible to enable only the MSCs you want with the `unstable-mscXXXX` features (where
65//! `XXXX` is the number of the MSC). When enabling an MSC, all MSC dependencies are enabled at the
66//! same time to avoid issues.
67//!
68//! Currently the extensible events use the unstable prefixes as defined in the corresponding MSCs.
69//!
70//! [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
71//! [MSC3954]: https://github.com/matrix-org/matrix-spec-proposals/pull/3954
72//! [MSC3955]: https://github.com/matrix-org/matrix-spec-proposals/pull/3955
73//! [MSC3956]: https://github.com/matrix-org/matrix-spec-proposals/pull/3956
74//! [MSC3551]: https://github.com/matrix-org/matrix-spec-proposals/pull/3551
75//! [MSC3552]: https://github.com/matrix-org/matrix-spec-proposals/pull/3552
76//! [MSC3553]: https://github.com/matrix-org/matrix-spec-proposals/pull/3553
77//! [MSC3927]: https://github.com/matrix-org/matrix-spec-proposals/pull/3927
78//! [MSC3488]: https://github.com/matrix-org/matrix-spec-proposals/pull/3488
79//! [MSC3245]: https://github.com/matrix-org/matrix-spec-proposals/pull/3245
80//! [MSC3246]: https://github.com/matrix-org/matrix-spec-proposals/pull/3246
81//! [MSC3381]: https://github.com/matrix-org/matrix-spec-proposals/pull/3381
82use std::ops::Deref;
83
84#[cfg(feature = "unstable-msc1767")]
85use ruma_macros::EventContent;
86use serde::{Deserialize, Serialize};
87
88#[cfg(feature = "unstable-msc1767")]
89use super::room::message::Relation;
90#[cfg(feature = "unstable-msc4095")]
91use super::room::message::UrlPreview;
92
93#[cfg(feature = "unstable-msc1767")]
94pub(super) mod historical_serde;
95
96/// The payload for an extensible text message.
97///
98/// This is the new primary type introduced in [MSC1767] and should only be sent in rooms with a
99/// version that supports it. See the documentation of the [`message`] module for more information.
100///
101/// To construct a `MessageEventContent` with a custom [`TextContentBlock`], convert it with
102/// `MessageEventContent::from()` / `.into()`.
103///
104/// [MSC1767]: https://github.com/matrix-org/matrix-spec-proposals/pull/1767
105/// [`message`]: super::message
106#[cfg(feature = "unstable-msc1767")]
107#[derive(Clone, Debug, Serialize, Deserialize, EventContent)]
108#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
109#[ruma_event(type = "org.matrix.msc1767.message", kind = MessageLike, without_relation)]
110pub struct MessageEventContent {
111 /// The message's text content.
112 #[serde(rename = "org.matrix.msc1767.text", alias = "m.text")]
113 pub text: TextContentBlock,
114
115 /// Whether this message is automated.
116 #[cfg(feature = "unstable-msc3955")]
117 #[serde(
118 default,
119 skip_serializing_if = "ruma_common::serde::is_default",
120 rename = "org.matrix.msc1767.automated"
121 )]
122 pub automated: bool,
123
124 /// Information about related messages.
125 #[serde(
126 flatten,
127 skip_serializing_if = "Option::is_none",
128 deserialize_with = "crate::room::message::relation_serde::deserialize_relation"
129 )]
130 pub relates_to: Option<Relation<MessageEventContentWithoutRelation>>,
131
132 /// [MSC4095](https://github.com/matrix-org/matrix-spec-proposals/pull/4095)-style bundled url previews
133 #[cfg(feature = "unstable-msc4095")]
134 #[serde(
135 rename = "com.beeper.linkpreviews",
136 skip_serializing_if = "Option::is_none",
137 alias = "m.url_previews"
138 )]
139 pub url_previews: Option<Vec<UrlPreview>>,
140}
141
142#[cfg(feature = "unstable-msc1767")]
143impl MessageEventContent {
144 /// A convenience constructor to create a plain text message.
145 pub fn plain(body: impl Into<String>) -> Self {
146 Self {
147 text: TextContentBlock::plain(body),
148 #[cfg(feature = "unstable-msc3955")]
149 automated: false,
150 relates_to: None,
151 #[cfg(feature = "unstable-msc4095")]
152 url_previews: None,
153 }
154 }
155
156 /// A convenience constructor to create an HTML message.
157 pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
158 Self {
159 text: TextContentBlock::html(body, html_body),
160 #[cfg(feature = "unstable-msc3955")]
161 automated: false,
162 relates_to: None,
163 #[cfg(feature = "unstable-msc4095")]
164 url_previews: None,
165 }
166 }
167
168 /// A convenience constructor to create a message from Markdown.
169 ///
170 /// The content includes an HTML message if some Markdown formatting was detected, otherwise
171 /// only a plain text message is included.
172 #[cfg(feature = "markdown")]
173 pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
174 Self {
175 text: TextContentBlock::markdown(body),
176 #[cfg(feature = "unstable-msc3955")]
177 automated: false,
178 relates_to: None,
179 #[cfg(feature = "unstable-msc4095")]
180 url_previews: None,
181 }
182 }
183}
184
185#[cfg(feature = "unstable-msc1767")]
186impl From<TextContentBlock> for MessageEventContent {
187 fn from(text: TextContentBlock) -> Self {
188 Self {
189 text,
190 #[cfg(feature = "unstable-msc3955")]
191 automated: false,
192 relates_to: None,
193 #[cfg(feature = "unstable-msc4095")]
194 url_previews: None,
195 }
196 }
197}
198
199/// A block for text content with optional markup.
200///
201/// This is an array of [`TextRepresentation`].
202///
203/// To construct a `TextContentBlock` with custom MIME types, construct a `Vec<TextRepresentation>`
204/// first and use its `::from()` / `.into()` implementation.
205#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206#[serde(transparent)]
207pub struct TextContentBlock(Vec<TextRepresentation>);
208
209impl TextContentBlock {
210 /// A convenience constructor to create a plain text message.
211 pub fn plain(body: impl Into<String>) -> Self {
212 Self(vec![TextRepresentation::plain(body)])
213 }
214
215 /// A convenience constructor to create an HTML message with a plain text fallback.
216 pub fn html(body: impl Into<String>, html_body: impl Into<String>) -> Self {
217 Self(vec![TextRepresentation::html(html_body), TextRepresentation::plain(body)])
218 }
219
220 /// A convenience constructor to create a message from Markdown.
221 ///
222 /// The content includes an HTML message if some Markdown formatting was detected, otherwise
223 /// only a plain text message is included.
224 #[cfg(feature = "markdown")]
225 pub fn markdown(body: impl AsRef<str> + Into<String>) -> Self {
226 let mut message = Vec::with_capacity(2);
227 if let Some(html_body) = TextRepresentation::markdown(&body) {
228 message.push(html_body);
229 }
230 message.push(TextRepresentation::plain(body));
231 Self(message)
232 }
233
234 /// Whether this content block is empty.
235 pub fn is_empty(&self) -> bool {
236 self.0.is_empty()
237 }
238
239 /// Get the plain text representation of this message.
240 pub fn find_plain(&self) -> Option<&str> {
241 self.iter()
242 .find(|content| content.mimetype == "text/plain")
243 .map(|content| content.body.as_ref())
244 }
245
246 /// Get the HTML representation of this message.
247 pub fn find_html(&self) -> Option<&str> {
248 self.iter()
249 .find(|content| content.mimetype == "text/html")
250 .map(|content| content.body.as_ref())
251 }
252}
253
254impl From<Vec<TextRepresentation>> for TextContentBlock {
255 fn from(representations: Vec<TextRepresentation>) -> Self {
256 Self(representations)
257 }
258}
259
260impl FromIterator<TextRepresentation> for TextContentBlock {
261 fn from_iter<T: IntoIterator<Item = TextRepresentation>>(iter: T) -> Self {
262 Self(iter.into_iter().collect())
263 }
264}
265
266impl Deref for TextContentBlock {
267 type Target = [TextRepresentation];
268
269 fn deref(&self) -> &Self::Target {
270 &self.0
271 }
272}
273
274/// Text content with optional markup.
275#[derive(Clone, Debug, Serialize, Deserialize)]
276#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
277pub struct TextRepresentation {
278 /// The MIME type of the `body`.
279 ///
280 /// This must follow the format defined in [RFC 6838].
281 ///
282 /// [RFC 6838]: https://datatracker.ietf.org/doc/html/rfc6838
283 #[serde(
284 default = "TextRepresentation::default_mimetype",
285 skip_serializing_if = "TextRepresentation::is_default_mimetype"
286 )]
287 pub mimetype: String,
288
289 /// The text content.
290 pub body: String,
291
292 /// The language of the text ([MSC3554]).
293 ///
294 /// This must be a valid language code according to [BCP 47](https://www.rfc-editor.org/rfc/bcp/bcp47.txt).
295 ///
296 /// This is optional and defaults to `en`.
297 ///
298 /// [MSC3554]: https://github.com/matrix-org/matrix-spec-proposals/pull/3554
299 #[cfg(feature = "unstable-msc3554")]
300 #[serde(
301 rename = "org.matrix.msc3554.lang",
302 default = "TextRepresentation::default_lang",
303 skip_serializing_if = "TextRepresentation::is_default_lang"
304 )]
305 pub lang: String,
306}
307
308impl TextRepresentation {
309 /// Creates a new `TextRepresentation` with the given MIME type and body.
310 pub fn new(mimetype: impl Into<String>, body: impl Into<String>) -> Self {
311 Self {
312 mimetype: mimetype.into(),
313 body: body.into(),
314 #[cfg(feature = "unstable-msc3554")]
315 lang: Self::default_lang(),
316 }
317 }
318
319 /// Creates a new plain text message body.
320 pub fn plain(body: impl Into<String>) -> Self {
321 Self::new("text/plain", body)
322 }
323
324 /// Creates a new HTML-formatted message body.
325 pub fn html(body: impl Into<String>) -> Self {
326 Self::new("text/html", body)
327 }
328
329 /// Creates a new HTML-formatted message body by parsing the Markdown in `body`.
330 ///
331 /// Returns `None` if no Markdown formatting was found.
332 #[cfg(feature = "markdown")]
333 pub fn markdown(body: impl AsRef<str>) -> Option<Self> {
334 use super::room::message::parse_markdown;
335
336 parse_markdown(body.as_ref()).map(Self::html)
337 }
338
339 fn default_mimetype() -> String {
340 "text/plain".to_owned()
341 }
342
343 fn is_default_mimetype(mime: &str) -> bool {
344 mime == "text/plain"
345 }
346
347 #[cfg(feature = "unstable-msc3554")]
348 fn default_lang() -> String {
349 "en".to_owned()
350 }
351
352 #[cfg(feature = "unstable-msc3554")]
353 fn is_default_lang(lang: &str) -> bool {
354 lang == "en"
355 }
356}