1use 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#[derive(Clone, Debug, Serialize)]
37#[allow(clippy::exhaustive_enums)]
38pub enum MediaSource {
39 #[serde(rename = "url")]
41 Plain(OwnedMxcUri),
42
43 #[serde(rename = "file")]
45 Encrypted(Box<EncryptedFile>),
46}
47
48impl<'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 MediaSourceJsonRepr { file: Some(file), .. } => Ok(MediaSource::Encrypted(file)),
67 MediaSourceJsonRepr { url: Some(url), .. } => Ok(MediaSource::Plain(url)),
68 }
69 }
70}
71
72#[derive(Clone, Debug, Default, Deserialize, Serialize)]
74#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
75pub struct ImageInfo {
76 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
78 pub height: Option<UInt>,
79
80 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
82 pub width: Option<UInt>,
83
84 #[serde(skip_serializing_if = "Option::is_none")]
86 pub mimetype: Option<String>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub size: Option<UInt>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub thumbnail_info: Option<Box<ThumbnailInfo>>,
95
96 #[serde(flatten, with = "thumbnail_source_serde", skip_serializing_if = "Option::is_none")]
98 pub thumbnail_source: Option<MediaSource>,
99
100 #[cfg(feature = "unstable-msc2448")]
105 #[serde(rename = "xyz.amorgan.blurhash", skip_serializing_if = "Option::is_none")]
106 pub blurhash: Option<String>,
107
108 #[cfg(feature = "unstable-msc2448")]
113 #[serde(rename = "xyz.amorgan.thumbhash", skip_serializing_if = "Option::is_none")]
114 pub thumbhash: Option<Base64>,
115
116 #[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 pub fn new() -> Self {
129 Self::default()
130 }
131}
132
133#[derive(Clone, Debug, Default, Deserialize, Serialize)]
135#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
136pub struct ThumbnailInfo {
137 #[serde(rename = "h", skip_serializing_if = "Option::is_none")]
139 pub height: Option<UInt>,
140
141 #[serde(rename = "w", skip_serializing_if = "Option::is_none")]
143 pub width: Option<UInt>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub mimetype: Option<String>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub size: Option<UInt>,
152}
153
154impl ThumbnailInfo {
155 pub fn new() -> Self {
157 Self::default()
158 }
159}
160
161#[derive(Clone, Debug, Deserialize, Serialize)]
166#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
167pub struct EncryptedFile {
168 pub url: OwnedMxcUri,
170
171 pub key: JsonWebKey,
173
174 pub iv: Base64,
176
177 pub hashes: BTreeMap<String, Base64>,
181
182 pub v: String,
186}
187
188#[derive(Debug)]
193#[allow(clippy::exhaustive_structs)]
194pub struct EncryptedFileInit {
195 pub url: OwnedMxcUri,
197
198 pub key: JsonWebKey,
200
201 pub iv: Base64,
203
204 pub hashes: BTreeMap<String, Base64>,
208
209 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#[derive(Clone, Debug, Deserialize, Serialize)]
227#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
228pub struct JsonWebKey {
229 pub kty: String,
233
234 pub key_ops: Vec<String>,
238
239 pub alg: String,
243
244 pub k: Base64<UrlSafe>,
246
247 pub ext: bool,
252}
253
254#[derive(Debug)]
259#[allow(clippy::exhaustive_structs)]
260pub struct JsonWebKeyInit {
261 pub kty: String,
265
266 pub key_ops: Vec<String>,
270
271 pub alg: String,
275
276 pub k: Base64<UrlSafe>,
278
279 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 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}