1use std::collections::BTreeMap;
4
5use js_int::UInt;
6use ruma_common::{
7 OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, OwnedUserId,
8 encryption::{CrossSigningKey, DeviceKeys},
9 presence::PresenceState,
10 serde::{Raw, from_raw_json_value},
11 to_device::DeviceIdOrAllDevices,
12};
13use ruma_events::{AnyToDeviceEventContent, ToDeviceEventType, receipt::Receipt};
14use serde::{Deserialize, Serialize, de};
15use serde_json::value::RawValue as RawJsonValue;
16
17#[derive(Clone, Debug, Serialize)]
19#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
20#[serde(tag = "edu_type", content = "content")]
21pub enum Edu {
22 #[serde(rename = "m.presence")]
24 Presence(PresenceContent),
25
26 #[serde(rename = "m.receipt")]
28 Receipt(ReceiptContent),
29
30 #[serde(rename = "m.typing")]
32 Typing(TypingContent),
33
34 #[serde(rename = "m.device_list_update")]
38 DeviceListUpdate(DeviceListUpdateContent),
39
40 #[serde(rename = "m.direct_to_device")]
44 DirectToDevice(DirectDeviceContent),
45
46 #[serde(rename = "m.signing_key_update")]
49 SigningKeyUpdate(SigningKeyUpdateContent),
50
51 #[doc(hidden)]
52 #[serde(untagged)]
53 _Custom(CustomEdu),
54}
55
56impl<'de> Deserialize<'de> for Edu {
57 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
58 where
59 D: de::Deserializer<'de>,
60 {
61 #[derive(Debug, Deserialize)]
62 struct EduDeHelper {
63 edu_type: String,
64 content: Box<RawJsonValue>,
65 }
66
67 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
68 let EduDeHelper { edu_type, content } = from_raw_json_value(&json)?;
69
70 Ok(match edu_type.as_ref() {
71 "m.presence" => Self::Presence(from_raw_json_value(&content)?),
72 "m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
73 "m.typing" => Self::Typing(from_raw_json_value(&content)?),
74 "m.device_list_update" => Self::DeviceListUpdate(from_raw_json_value(&content)?),
75 "m.direct_to_device" => Self::DirectToDevice(from_raw_json_value(&content)?),
76 "m.signing_key_update" => Self::SigningKeyUpdate(from_raw_json_value(&content)?),
77 _ => Self::_Custom(CustomEdu { edu_type, content }),
78 })
79 }
80}
81
82#[derive(Clone, Debug, Deserialize, Serialize)]
84#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
85pub struct PresenceContent {
86 pub push: Vec<PresenceUpdate>,
88}
89
90impl PresenceContent {
91 pub fn new(push: Vec<PresenceUpdate>) -> Self {
93 Self { push }
94 }
95}
96
97#[derive(Clone, Debug, Deserialize, Serialize)]
99#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
100pub struct PresenceUpdate {
101 pub user_id: OwnedUserId,
103
104 pub presence: PresenceState,
106
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub status_msg: Option<String>,
110
111 pub last_active_ago: UInt,
113
114 #[serde(default)]
118 pub currently_active: bool,
119}
120
121impl PresenceUpdate {
122 pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self {
124 Self {
125 user_id,
126 presence,
127 last_active_ago: last_activity,
128 status_msg: None,
129 currently_active: false,
130 }
131 }
132}
133
134#[derive(Clone, Debug, Deserialize, Serialize)]
136#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
137pub struct ReceiptContent {
138 #[serde(flatten)]
140 pub receipts: BTreeMap<OwnedRoomId, ReceiptMap>,
141}
142
143impl ReceiptContent {
144 pub fn new(receipts: BTreeMap<OwnedRoomId, ReceiptMap>) -> Self {
146 Self { receipts }
147 }
148}
149
150#[derive(Clone, Debug, Deserialize, Serialize)]
152#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
153pub struct ReceiptMap {
154 #[serde(rename = "m.read")]
156 pub read: BTreeMap<OwnedUserId, ReceiptData>,
157}
158
159impl ReceiptMap {
160 pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
162 Self { read }
163 }
164}
165
166#[derive(Clone, Debug, Deserialize, Serialize)]
168#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
169pub struct ReceiptData {
170 pub data: Receipt,
172
173 pub event_ids: Vec<OwnedEventId>,
175}
176
177impl ReceiptData {
178 pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
180 Self { data, event_ids }
181 }
182}
183
184#[derive(Clone, Debug, Deserialize, Serialize)]
186#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
187pub struct TypingContent {
188 pub room_id: OwnedRoomId,
190
191 pub user_id: OwnedUserId,
193
194 pub typing: bool,
196}
197
198impl TypingContent {
199 pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self {
201 Self { room_id, user_id, typing }
202 }
203}
204
205#[derive(Clone, Debug, Deserialize, Serialize)]
207#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
208pub struct DeviceListUpdateContent {
209 pub user_id: OwnedUserId,
211
212 pub device_id: OwnedDeviceId,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
219 pub device_display_name: Option<String>,
220
221 pub stream_id: UInt,
223
224 #[serde(default, skip_serializing_if = "Vec::is_empty")]
227 pub prev_id: Vec<UInt>,
228
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub deleted: Option<bool>,
232
233 #[serde(skip_serializing_if = "Option::is_none")]
235 pub keys: Option<Raw<DeviceKeys>>,
236}
237
238impl DeviceListUpdateContent {
239 pub fn new(user_id: OwnedUserId, device_id: OwnedDeviceId, stream_id: UInt) -> Self {
242 Self {
243 user_id,
244 device_id,
245 device_display_name: None,
246 stream_id,
247 prev_id: vec![],
248 deleted: None,
249 keys: None,
250 }
251 }
252}
253
254#[derive(Clone, Debug, Deserialize, Serialize)]
256#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
257pub struct DirectDeviceContent {
258 pub sender: OwnedUserId,
260
261 #[serde(rename = "type")]
263 pub ev_type: ToDeviceEventType,
264
265 pub message_id: OwnedTransactionId,
267
268 pub messages: DirectDeviceMessages,
273}
274
275impl DirectDeviceContent {
276 pub fn new(
278 sender: OwnedUserId,
279 ev_type: ToDeviceEventType,
280 message_id: OwnedTransactionId,
281 ) -> Self {
282 Self { sender, ev_type, message_id, messages: DirectDeviceMessages::new() }
283 }
284}
285
286pub type DirectDeviceMessages =
290 BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>;
291
292#[derive(Clone, Debug, Deserialize, Serialize)]
294#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
295pub struct SigningKeyUpdateContent {
296 pub user_id: OwnedUserId,
298
299 #[serde(skip_serializing_if = "Option::is_none")]
301 pub master_key: Option<Raw<CrossSigningKey>>,
302
303 #[serde(skip_serializing_if = "Option::is_none")]
305 pub self_signing_key: Option<Raw<CrossSigningKey>>,
306}
307
308impl SigningKeyUpdateContent {
309 pub fn new(user_id: OwnedUserId) -> Self {
311 Self { user_id, master_key: None, self_signing_key: None }
312 }
313}
314
315#[doc(hidden)]
317#[derive(Clone, Debug, Serialize)]
318pub struct CustomEdu {
319 edu_type: String,
321
322 content: Box<RawJsonValue>,
324}
325
326#[cfg(test)]
327mod tests {
328 use assert_matches2::assert_matches;
329 use js_int::uint;
330 use ruma_common::{room_id, user_id};
331 use ruma_events::ToDeviceEventType;
332 use serde_json::json;
333
334 use super::{DeviceListUpdateContent, Edu, ReceiptContent};
335
336 #[test]
337 fn device_list_update_edu() {
338 let json = json!({
339 "content": {
340 "deleted": false,
341 "device_display_name": "Mobile",
342 "device_id": "QBUAZIFURK",
343 "keys": {
344 "algorithms": [
345 "m.olm.v1.curve25519-aes-sha2",
346 "m.megolm.v1.aes-sha2"
347 ],
348 "device_id": "JLAFKJWSCS",
349 "keys": {
350 "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
351 "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
352 },
353 "signatures": {
354 "@alice:example.com": {
355 "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
356 }
357 },
358 "user_id": "@alice:example.com"
359 },
360 "stream_id": 6,
361 "user_id": "@john:example.com"
362 },
363 "edu_type": "m.device_list_update"
364 });
365
366 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
367 assert_matches!(
368 &edu,
369 Edu::DeviceListUpdate(DeviceListUpdateContent {
370 user_id,
371 device_id,
372 device_display_name,
373 stream_id,
374 prev_id,
375 deleted,
376 keys,
377 })
378 );
379
380 assert_eq!(user_id, "@john:example.com");
381 assert_eq!(device_id, "QBUAZIFURK");
382 assert_eq!(device_display_name.as_deref(), Some("Mobile"));
383 assert_eq!(*stream_id, uint!(6));
384 assert_eq!(*prev_id, vec![]);
385 assert_eq!(*deleted, Some(false));
386 assert_matches!(keys, Some(_));
387
388 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
389 }
390
391 #[test]
392 fn minimal_device_list_update_edu() {
393 let json = json!({
394 "content": {
395 "device_id": "QBUAZIFURK",
396 "stream_id": 6,
397 "user_id": "@john:example.com"
398 },
399 "edu_type": "m.device_list_update"
400 });
401
402 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
403 assert_matches!(
404 &edu,
405 Edu::DeviceListUpdate(DeviceListUpdateContent {
406 user_id,
407 device_id,
408 device_display_name,
409 stream_id,
410 prev_id,
411 deleted,
412 keys,
413 })
414 );
415
416 assert_eq!(user_id, "@john:example.com");
417 assert_eq!(device_id, "QBUAZIFURK");
418 assert_eq!(*device_display_name, None);
419 assert_eq!(*stream_id, uint!(6));
420 assert_eq!(*prev_id, vec![]);
421 assert_eq!(*deleted, None);
422 assert_matches!(keys, None);
423
424 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
425 }
426
427 #[test]
428 fn receipt_edu() {
429 let json = json!({
430 "content": {
431 "!some_room:example.org": {
432 "m.read": {
433 "@john:matrix.org": {
434 "data": {
435 "ts": 1_533_358
436 },
437 "event_ids": [
438 "$read_this_event:matrix.org"
439 ]
440 }
441 }
442 }
443 },
444 "edu_type": "m.receipt"
445 });
446
447 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
448 assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
449 assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
450
451 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
452 }
453
454 #[test]
455 fn typing_edu() {
456 let json = json!({
457 "content": {
458 "room_id": "!somewhere:matrix.org",
459 "typing": true,
460 "user_id": "@john:matrix.org"
461 },
462 "edu_type": "m.typing"
463 });
464
465 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
466 assert_matches!(&edu, Edu::Typing(content));
467 assert_eq!(content.room_id, "!somewhere:matrix.org");
468 assert_eq!(content.user_id, "@john:matrix.org");
469 assert!(content.typing);
470
471 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
472 }
473
474 #[test]
475 fn direct_to_device_edu() {
476 let json = json!({
477 "content": {
478 "message_id": "hiezohf6Hoo7kaev",
479 "messages": {
480 "@alice:example.org": {
481 "IWHQUZUIAH": {
482 "algorithm": "m.megolm.v1.aes-sha2",
483 "room_id": "!Cuyf34gef24t:localhost",
484 "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
485 "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
486 }
487 }
488 },
489 "sender": "@john:example.com",
490 "type": "m.room_key_request"
491 },
492 "edu_type": "m.direct_to_device"
493 });
494
495 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
496 assert_matches!(&edu, Edu::DirectToDevice(content));
497 assert_eq!(content.sender, "@john:example.com");
498 assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
499 assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
500 assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
501
502 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
503 }
504
505 #[test]
506 fn signing_key_update_edu() {
507 let json = json!({
508 "content": {
509 "master_key": {
510 "keys": {
511 "ed25519:alice+base64+public+key": "alice+base64+public+key",
512 "ed25519:base64+master+public+key": "base64+master+public+key"
513 },
514 "signatures": {
515 "@alice:example.com": {
516 "ed25519:alice+base64+master+key": "signature+of+key"
517 }
518 },
519 "usage": [
520 "master"
521 ],
522 "user_id": "@alice:example.com"
523 },
524 "self_signing_key": {
525 "keys": {
526 "ed25519:alice+base64+public+key": "alice+base64+public+key",
527 "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
528 },
529 "signatures": {
530 "@alice:example.com": {
531 "ed25519:alice+base64+master+key": "signature+of+key",
532 "ed25519:base64+master+public+key": "signature+of+self+signing+key"
533 }
534 },
535 "usage": [
536 "self_signing"
537 ],
538 "user_id": "@alice:example.com"
539 },
540 "user_id": "@alice:example.com"
541 },
542 "edu_type": "m.signing_key_update"
543 });
544
545 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
546 assert_matches!(&edu, Edu::SigningKeyUpdate(content));
547 assert_eq!(content.user_id, "@alice:example.com");
548 assert!(content.master_key.is_some());
549 assert!(content.self_signing_key.is_some());
550
551 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
552 }
553}