ruma_events/
room.rs

1//! Modules for events in the `m.room` namespace.
2//!
3//! This module also contains types shared by events in its child namespaces.
4
5use std::collections::BTreeMap;
6
7use js_int::UInt;
8use ruma_common::{
9    serde::{base64::UrlSafe, Base64},
10    OwnedMxcUri,
11};
12use serde::{de, Deserialize, Serialize};
13use zeroize::Zeroize;
14
15pub mod aliases;
16pub mod avatar;
17pub mod canonical_alias;
18pub mod create;
19pub mod encrypted;
20pub mod encryption;
21pub mod guest_access;
22pub mod history_visibility;
23pub mod join_rules;
24#[cfg(feature = "unstable-msc4334")]
25pub mod language;
26pub mod member;
27pub mod message;
28pub mod name;
29pub mod pinned_events;
30pub mod power_levels;
31pub mod redaction;
32pub mod server_acl;
33pub mod third_party_invite;
34mod thumbnail_source_serde;
35pub mod tombstone;
36pub mod topic;
37
38/// The source of a media file.
39#[derive(Clone, Debug, Serialize)]
40#[allow(clippy::exhaustive_enums)]
41pub enum MediaSource {
42    /// The MXC URI to the unencrypted media file.
43    #[serde(rename = "url")]
44    Plain(OwnedMxcUri),
45
46    /// The encryption info of the encrypted media file.
47    #[serde(rename = "file")]
48    Encrypted(Box<EncryptedFile>),
49}
50
51// Custom implementation of `Deserialize`, because serde doesn't guarantee what variant will be
52// deserialized for "externally tagged"¹ enums where multiple "tag" fields exist.
53//
54// ¹ https://serde.rs/enum-representations.html
55impl<'de> Deserialize<'de> for MediaSource {
56    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
57    where
58        D: serde::Deserializer<'de>,
59    {
60        #[derive(Deserialize)]
61        struct MediaSourceJsonRepr {
62            url: Option<OwnedMxcUri>,
63            file: Option<Box<EncryptedFile>>,
64        }
65
66        match MediaSourceJsonRepr::deserialize(deserializer)? {
67            MediaSourceJsonRepr { url: None, file: None } => Err(de::Error::missing_field("url")),
68            // Prefer file if it is set
69            MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
70            MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
71        }
72    }
73}
74
75/// Metadata about an image.
76#[derive(Clone, Debug, Default, Deserialize, Serialize)]
77#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
78pub struct ImageInfo {
79    /// The height of the image in pixels.
80    #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
81    pub height: Option<UInt>,
82
83    /// The width of the image in pixels.
84    #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
85    pub width: Option<UInt>,
86
87    /// The MIME type of the image, e.g. "image/png."
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub mimetype: Option<String>,
90
91    /// The file size of the image in bytes.
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub size: Option<UInt>,
94
95    /// Metadata about the image referred to in `thumbnail_source`.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub thumbnail_info: Option<Box<ThumbnailInfo>>,
98
99    /// The source of the thumbnail of the image.
100    #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
101    pub thumbnail_source: Option<MediaSource>,
102
103    /// The [BlurHash](https://blurha.sh) for this image.
104    ///
105    /// This uses the unstable prefix in
106    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
107    #[cfg(feature = "unstable-msc2448")]
108    #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
109    pub blurhash: Option<String>,
110
111    /// The [ThumbHash](https://evanw.github.io/thumbhash/) for this image.
112    ///
113    /// This uses the unstable prefix in
114    /// [MSC2448](https://github.com/matrix-org/matrix-spec-proposals/pull/2448).
115    #[cfg(feature = "unstable-msc2448")]
116    #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
117    pub thumbhash: Option<Base64>,
118
119    /// Whether the image is animated.
120    ///
121    /// This uses the unstable prefix in [MSC4230].
122    ///
123    /// [MSC4230]: https://github.com/matrix-org/matrix-spec-proposals/pull/4230
124    #[cfg(feature = "unstable-msc4230")]
125    #[serde(rename = "org.matrix.msc4230.is_animated", skip_serializing_if = "Option::is_none")]
126    pub is_animated: Option<bool>,
127}
128
129impl ImageInfo {
130    /// Creates an empty `ImageInfo`.
131    pub fn new() -> Self {
132        Self::default()
133    }
134}
135
136/// Metadata about a thumbnail.
137#[derive(Clone, Debug, Default, Deserialize, Serialize)]
138#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
139pub struct ThumbnailInfo {
140    /// The height of the thumbnail in pixels.
141    #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
142    pub height: Option<UInt>,
143
144    /// The width of the thumbnail in pixels.
145    #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
146    pub width: Option<UInt>,
147
148    /// The MIME type of the thumbnail, e.g. "image/png."
149    #[serde(skip_serializing_if = "Option::is_none")]
150    pub mimetype: Option<String>,
151
152    /// The file size of the thumbnail in bytes.
153    #[serde(skip_serializing_if = "Option::is_none")]
154    pub size: Option<UInt>,
155}
156
157impl ThumbnailInfo {
158    /// Creates an empty `ThumbnailInfo`.
159    pub fn new() -> Self {
160        Self::default()
161    }
162}
163
164/// A file sent to a room with end-to-end encryption enabled.
165///
166/// To create an instance of this type, first create a `EncryptedFileInit` and convert it via
167/// `EncryptedFile::from` / `.into()`.
168#[derive(Clone, Debug, Deserialize, Serialize)]
169#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
170pub struct EncryptedFile {
171    /// The URL to the file.
172    pub url: OwnedMxcUri,
173
174    /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object.
175    pub key: JsonWebKey,
176
177    /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
178    pub iv: Base64,
179
180    /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
181    ///
182    /// Clients should support the SHA-256 hash, which uses the key sha256.
183    pub hashes: BTreeMap<String, Base64>,
184
185    /// Version of the encrypted attachments protocol.
186    ///
187    /// Must be `v2`.
188    pub v: String,
189}
190
191/// Initial set of fields of `EncryptedFile`.
192///
193/// This struct will not be updated even if additional fields are added to `EncryptedFile` in a new
194/// (non-breaking) release of the Matrix specification.
195#[derive(Debug)]
196#[allow(clippy::exhaustive_structs)]
197pub struct EncryptedFileInit {
198    /// The URL to the file.
199    pub url: OwnedMxcUri,
200
201    /// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object.
202    pub key: JsonWebKey,
203
204    /// The 128-bit unique counter block used by AES-CTR, encoded as unpadded base64.
205    pub iv: Base64,
206
207    /// A map from an algorithm name to a hash of the ciphertext, encoded as unpadded base64.
208    ///
209    /// Clients should support the SHA-256 hash, which uses the key sha256.
210    pub hashes: BTreeMap<String, Base64>,
211
212    /// Version of the encrypted attachments protocol.
213    ///
214    /// Must be `v2`.
215    pub v: String,
216}
217
218impl From<EncryptedFileInit> for EncryptedFile {
219    fn from(init: EncryptedFileInit) -> Self {
220        let EncryptedFileInit { url, key, iv, hashes, v } = init;
221        Self { url, key, iv, hashes, v }
222    }
223}
224
225/// A [JSON Web Key](https://tools.ietf.org/html/rfc7517#appendix-A.3) object.
226///
227/// To create an instance of this type, first create a `JsonWebKeyInit` and convert it via
228/// `JsonWebKey::from` / `.into()`.
229#[derive(Clone, Deserialize, Serialize)]
230#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
231pub struct JsonWebKey {
232    /// Key type.
233    ///
234    /// Must be `oct`.
235    pub kty: String,
236
237    /// Key operations.
238    ///
239    /// Must at least contain `encrypt` and `decrypt`.
240    pub key_ops: Vec<String>,
241
242    /// Algorithm.
243    ///
244    /// Must be `A256CTR`.
245    pub alg: String,
246
247    /// The key, encoded as url-safe unpadded base64.
248    pub k: Base64<UrlSafe>,
249
250    /// Extractable.
251    ///
252    /// Must be `true`. This is a
253    /// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk).
254    pub ext: bool,
255}
256
257impl std::fmt::Debug for JsonWebKey {
258    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
259        f.debug_struct("JsonWebKey")
260            .field("kty", &self.kty)
261            .field("key_ops", &self.key_ops)
262            .field("alg", &self.alg)
263            .field("ext", &self.ext)
264            .finish_non_exhaustive()
265    }
266}
267
268impl Drop for JsonWebKey {
269    fn drop(&mut self) {
270        self.k.zeroize();
271    }
272}
273
274/// Initial set of fields of `JsonWebKey`.
275///
276/// This struct will not be updated even if additional fields are added to `JsonWebKey` in a new
277/// (non-breaking) release of the Matrix specification.
278#[allow(clippy::exhaustive_structs)]
279pub struct JsonWebKeyInit {
280    /// Key type.
281    ///
282    /// Must be `oct`.
283    pub kty: String,
284
285    /// Key operations.
286    ///
287    /// Must at least contain `encrypt` and `decrypt`.
288    pub key_ops: Vec<String>,
289
290    /// Algorithm.
291    ///
292    /// Must be `A256CTR`.
293    pub alg: String,
294
295    /// The key, encoded as url-safe unpadded base64.
296    pub k: Base64<UrlSafe>,
297
298    /// Extractable.
299    ///
300    /// Must be `true`. This is a
301    /// [W3C extension](https://w3c.github.io/webcrypto/#iana-section-jwk).
302    pub ext: bool,
303}
304
305impl std::fmt::Debug for JsonWebKeyInit {
306    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
307        f.debug_struct("JsonWebKeyInit")
308            .field("kty", &self.kty)
309            .field("key_ops", &self.key_ops)
310            .field("alg", &self.alg)
311            .field("ext", &self.ext)
312            .finish_non_exhaustive()
313    }
314}
315
316impl From<JsonWebKeyInit> for JsonWebKey {
317    fn from(init: JsonWebKeyInit) -> Self {
318        let JsonWebKeyInit { kty, key_ops, alg, k, ext } = init;
319        Self { kty, key_ops, alg, k, ext }
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use std::collections::BTreeMap;
326
327    use assert_matches2::assert_matches;
328    use ruma_common::{mxc_uri, serde::Base64};
329    use serde::Deserialize;
330    use serde_json::{from_value as from_json_value, json};
331
332    use super::{EncryptedFile, JsonWebKey, MediaSource};
333
334    #[derive(Deserialize)]
335    struct MsgWithAttachment {
336        #[allow(dead_code)]
337        body: String,
338        #[serde(flatten)]
339        source: MediaSource,
340    }
341
342    fn dummy_jwt() -> JsonWebKey {
343        JsonWebKey {
344            kty: "oct".to_owned(),
345            key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
346            alg: "A256CTR".to_owned(),
347            k: Base64::new(vec![0; 64]),
348            ext: true,
349        }
350    }
351
352    fn encrypted_file() -> EncryptedFile {
353        EncryptedFile {
354            url: mxc_uri!("mxc://localhost/encryptedfile").to_owned(),
355            key: dummy_jwt(),
356            iv: Base64::new(vec![0; 64]),
357            hashes: BTreeMap::new(),
358            v: "v2".to_owned(),
359        }
360    }
361
362    #[test]
363    fn prefer_encrypted_attachment_over_plain() {
364        let msg: MsgWithAttachment = from_json_value(json!({
365            "body": "",
366            "url": "mxc://localhost/file",
367            "file": encrypted_file(),
368        }))
369        .unwrap();
370
371        assert_matches!(msg.source, MediaSource::Encrypted(_));
372
373        // As above, but with the file field before the url field
374        let msg: MsgWithAttachment = from_json_value(json!({
375            "body": "",
376            "file": encrypted_file(),
377            "url": "mxc://localhost/file",
378        }))
379        .unwrap();
380
381        assert_matches!(msg.source, MediaSource::Encrypted(_));
382    }
383}