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