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