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}