1use std::collections::BTreeMap;
4
5#[cfg(feature = "unstable-msc4495")]
6use js_int::Int;
7use js_int::UInt;
8use ruma_common::{
9 OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, OwnedUserId,
10 encryption::{CrossSigningKey, DeviceKeys},
11 presence::PresenceState,
12 serde::{Raw, from_raw_json_value},
13 to_device::DeviceIdOrAllDevices,
14};
15use ruma_events::{AnyToDeviceEventContent, ToDeviceEventType, receipt::Receipt};
16use serde::{Deserialize, Serialize, de};
17use serde_json::value::RawValue as RawJsonValue;
18
19#[derive(Clone, Debug, Serialize)]
21#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
22#[serde(tag = "edu_type", content = "content")]
23pub enum Edu {
24 #[serde(rename = "m.presence")]
26 Presence(PresenceContent),
27
28 #[serde(rename = "m.receipt")]
30 Receipt(ReceiptContent),
31
32 #[serde(rename = "m.typing")]
34 Typing(TypingContent),
35
36 #[serde(rename = "m.device_list_update")]
40 DeviceListUpdate(DeviceListUpdateContent),
41
42 #[serde(rename = "m.direct_to_device")]
46 DirectToDevice(DirectDeviceContent),
47
48 #[serde(rename = "m.signing_key_update")]
51 SigningKeyUpdate(SigningKeyUpdateContent),
52
53 #[doc(hidden)]
54 #[serde(untagged)]
55 _Custom(CustomEdu),
56}
57
58impl<'de> Deserialize<'de> for Edu {
59 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
60 where
61 D: de::Deserializer<'de>,
62 {
63 #[derive(Debug, Deserialize)]
64 struct EduDeHelper {
65 edu_type: String,
66 content: Box<RawJsonValue>,
67 }
68
69 let json = Box::<RawJsonValue>::deserialize(deserializer)?;
70 let EduDeHelper { edu_type, content } = from_raw_json_value(&json)?;
71
72 Ok(match edu_type.as_ref() {
73 "m.presence" => Self::Presence(from_raw_json_value(&content)?),
74 "m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
75 "m.typing" => Self::Typing(from_raw_json_value(&content)?),
76 "m.device_list_update" => Self::DeviceListUpdate(from_raw_json_value(&content)?),
77 "m.direct_to_device" => Self::DirectToDevice(from_raw_json_value(&content)?),
78 "m.signing_key_update" => Self::SigningKeyUpdate(from_raw_json_value(&content)?),
79 _ => Self::_Custom(CustomEdu { edu_type, content }),
80 })
81 }
82}
83
84#[derive(Clone, Debug, Deserialize, Serialize)]
86#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
87pub struct PresenceContent {
88 pub push: Vec<PresenceUpdate>,
90}
91
92impl PresenceContent {
93 pub fn new(push: Vec<PresenceUpdate>) -> Self {
95 Self { push }
96 }
97}
98
99#[derive(Clone, Default, Debug, Deserialize, Serialize)]
101#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
102#[cfg(feature = "unstable-msc4495")]
103pub struct PresenceRecipientListUpdates {
104 pub add: Vec<OwnedUserId>,
106
107 pub delete: Vec<OwnedUserId>,
109}
110
111#[cfg(feature = "unstable-msc4495")]
112impl PresenceRecipientListUpdates {
113 pub fn new(add: Vec<OwnedUserId>, delete: Vec<OwnedUserId>) -> Self {
115 Self { add, delete }
116 }
117
118 pub fn is_empty(&self) -> bool {
120 self.add.is_empty() && self.delete.is_empty()
121 }
122}
123
124#[derive(Clone, Debug, Deserialize, Serialize)]
126#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
127pub struct PresenceUpdate {
128 pub user_id: OwnedUserId,
130
131 pub presence: PresenceState,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub status_msg: Option<String>,
137
138 pub last_active_ago: UInt,
140
141 #[serde(default)]
145 pub currently_active: bool,
146
147 #[cfg(feature = "unstable-msc4495")]
155 #[serde(default, skip_serializing_if = "PresenceRecipientListUpdates::is_empty")]
156 pub recipients: PresenceRecipientListUpdates,
157
158 #[cfg(feature = "unstable-msc4495")]
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub stream_id: Option<Int>,
166
167 #[cfg(feature = "unstable-msc4495")]
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub prev_id: Option<Int>,
178}
179
180impl PresenceUpdate {
181 pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self {
183 Self {
184 user_id,
185 presence,
186 last_active_ago: last_activity,
187 status_msg: None,
188 currently_active: false,
189 #[cfg(feature = "unstable-msc4495")]
190 recipients: PresenceRecipientListUpdates::default(),
191 #[cfg(feature = "unstable-msc4495")]
192 stream_id: None,
193 #[cfg(feature = "unstable-msc4495")]
194 prev_id: None,
195 }
196 }
197}
198
199#[derive(Clone, Debug, Deserialize, Serialize)]
201#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
202pub struct ReceiptContent {
203 #[serde(flatten)]
205 pub receipts: BTreeMap<OwnedRoomId, ReceiptMap>,
206}
207
208impl ReceiptContent {
209 pub fn new(receipts: BTreeMap<OwnedRoomId, ReceiptMap>) -> Self {
211 Self { receipts }
212 }
213}
214
215#[derive(Clone, Debug, Deserialize, Serialize)]
217#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
218pub struct ReceiptMap {
219 #[serde(rename = "m.read")]
221 pub read: BTreeMap<OwnedUserId, ReceiptData>,
222}
223
224impl ReceiptMap {
225 pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
227 Self { read }
228 }
229}
230
231#[derive(Clone, Debug, Deserialize, Serialize)]
233#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
234pub struct ReceiptData {
235 pub data: Receipt,
237
238 pub event_ids: Vec<OwnedEventId>,
240}
241
242impl ReceiptData {
243 pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
245 Self { data, event_ids }
246 }
247}
248
249#[derive(Clone, Debug, Deserialize, Serialize)]
251#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
252pub struct TypingContent {
253 pub room_id: OwnedRoomId,
255
256 pub user_id: OwnedUserId,
258
259 pub typing: bool,
261}
262
263impl TypingContent {
264 pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self {
266 Self { room_id, user_id, typing }
267 }
268}
269
270#[derive(Clone, Debug, Deserialize, Serialize)]
272#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
273pub struct DeviceListUpdateContent {
274 pub user_id: OwnedUserId,
276
277 pub device_id: OwnedDeviceId,
279
280 #[serde(skip_serializing_if = "Option::is_none")]
284 pub device_display_name: Option<String>,
285
286 pub stream_id: UInt,
288
289 #[serde(default, skip_serializing_if = "Vec::is_empty")]
292 pub prev_id: Vec<UInt>,
293
294 #[serde(skip_serializing_if = "Option::is_none")]
296 pub deleted: Option<bool>,
297
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub keys: Option<Raw<DeviceKeys>>,
301}
302
303impl DeviceListUpdateContent {
304 pub fn new(user_id: OwnedUserId, device_id: OwnedDeviceId, stream_id: UInt) -> Self {
307 Self {
308 user_id,
309 device_id,
310 device_display_name: None,
311 stream_id,
312 prev_id: vec![],
313 deleted: None,
314 keys: None,
315 }
316 }
317}
318
319#[derive(Clone, Debug, Deserialize, Serialize)]
321#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
322pub struct DirectDeviceContent {
323 pub sender: OwnedUserId,
325
326 #[serde(rename = "type")]
328 pub ev_type: ToDeviceEventType,
329
330 pub message_id: OwnedTransactionId,
332
333 pub messages: DirectDeviceMessages,
338}
339
340impl DirectDeviceContent {
341 pub fn new(
343 sender: OwnedUserId,
344 ev_type: ToDeviceEventType,
345 message_id: OwnedTransactionId,
346 ) -> Self {
347 Self { sender, ev_type, message_id, messages: DirectDeviceMessages::new() }
348 }
349}
350
351pub type DirectDeviceMessages =
355 BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>;
356
357#[derive(Clone, Debug, Deserialize, Serialize)]
359#[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
360pub struct SigningKeyUpdateContent {
361 pub user_id: OwnedUserId,
363
364 #[serde(skip_serializing_if = "Option::is_none")]
366 pub master_key: Option<Raw<CrossSigningKey>>,
367
368 #[serde(skip_serializing_if = "Option::is_none")]
370 pub self_signing_key: Option<Raw<CrossSigningKey>>,
371}
372
373impl SigningKeyUpdateContent {
374 pub fn new(user_id: OwnedUserId) -> Self {
376 Self { user_id, master_key: None, self_signing_key: None }
377 }
378}
379
380#[doc(hidden)]
382#[derive(Clone, Debug, Serialize)]
383pub struct CustomEdu {
384 edu_type: String,
386
387 content: Box<RawJsonValue>,
389}
390
391#[cfg(test)]
392mod tests {
393 use assert_matches::assert_matches;
394 use js_int::uint;
395 use ruma_common::{canonical_json::assert_to_canonical_json_eq, presence::PresenceState};
396 use ruma_events::ToDeviceEventType;
397 use serde_json::json;
398 use strass::assert_let;
399
400 use super::{DeviceListUpdateContent, Edu, ReceiptContent};
401
402 #[test]
403 fn device_list_update_edu() {
404 let json = json!({
405 "content": {
406 "deleted": false,
407 "device_display_name": "Mobile",
408 "device_id": "QBUAZIFURK",
409 "keys": {
410 "algorithms": [
411 "m.olm.v1.curve25519-aes-sha2",
412 "m.megolm.v1.aes-sha2"
413 ],
414 "device_id": "JLAFKJWSCS",
415 "keys": {
416 "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
417 "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
418 },
419 "signatures": {
420 "@alice:example.com": {
421 "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
422 }
423 },
424 "user_id": "@alice:example.com"
425 },
426 "stream_id": 6,
427 "user_id": "@john:example.com"
428 },
429 "edu_type": "m.device_list_update"
430 });
431
432 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
433 assert_let!(
434 Edu::DeviceListUpdate(DeviceListUpdateContent {
435 user_id,
436 device_id,
437 device_display_name,
438 stream_id,
439 prev_id,
440 deleted,
441 keys,
442 }) = &edu
443 );
444
445 assert_eq!(user_id, "@john:example.com");
446 assert_eq!(device_id, "QBUAZIFURK");
447 assert_eq!(device_display_name.as_deref(), Some("Mobile"));
448 assert_eq!(*stream_id, uint!(6));
449 assert_eq!(*prev_id, vec![]);
450 assert_eq!(*deleted, Some(false));
451 assert_matches!(keys, Some(_));
452
453 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
454 }
455
456 #[test]
457 fn minimal_device_list_update_edu() {
458 let json = json!({
459 "content": {
460 "device_id": "QBUAZIFURK",
461 "stream_id": 6,
462 "user_id": "@john:example.com"
463 },
464 "edu_type": "m.device_list_update"
465 });
466
467 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
468 assert_let!(
469 Edu::DeviceListUpdate(DeviceListUpdateContent {
470 user_id,
471 device_id,
472 device_display_name,
473 stream_id,
474 prev_id,
475 deleted,
476 keys,
477 }) = &edu
478 );
479
480 assert_eq!(user_id, "@john:example.com");
481 assert_eq!(device_id, "QBUAZIFURK");
482 assert_eq!(*device_display_name, None);
483 assert_eq!(*stream_id, uint!(6));
484 assert_eq!(*prev_id, vec![]);
485 assert_eq!(*deleted, None);
486 assert_matches!(keys, None);
487
488 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
489 }
490
491 #[test]
492 fn receipt_edu() {
493 let json = json!({
494 "content": {
495 "!some_room:example.org": {
496 "m.read": {
497 "@john:matrix.org": {
498 "data": {
499 "ts": 1_533_358
500 },
501 "event_ids": [
502 "$read_this_event:matrix.org"
503 ]
504 }
505 }
506 }
507 },
508 "edu_type": "m.receipt"
509 });
510
511 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
512 assert_let!(Edu::Receipt(ReceiptContent { receipts }) = &edu);
513 assert!(receipts.get("!some_room:example.org").is_some());
514
515 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
516 }
517
518 #[test]
519 fn typing_edu() {
520 let json = json!({
521 "content": {
522 "room_id": "!somewhere:matrix.org",
523 "typing": true,
524 "user_id": "@john:matrix.org"
525 },
526 "edu_type": "m.typing"
527 });
528
529 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
530 assert_let!(Edu::Typing(content) = &edu);
531 assert_eq!(content.room_id, "!somewhere:matrix.org");
532 assert_eq!(content.user_id, "@john:matrix.org");
533 assert!(content.typing);
534
535 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
536 }
537
538 #[test]
539 fn direct_to_device_edu() {
540 let json = json!({
541 "content": {
542 "message_id": "hiezohf6Hoo7kaev",
543 "messages": {
544 "@alice:example.org": {
545 "IWHQUZUIAH": {
546 "algorithm": "m.megolm.v1.aes-sha2",
547 "room_id": "!Cuyf34gef24t:localhost",
548 "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
549 "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
550 }
551 }
552 },
553 "sender": "@john:example.com",
554 "type": "m.room_key_request"
555 },
556 "edu_type": "m.direct_to_device"
557 });
558
559 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
560 assert_let!(Edu::DirectToDevice(content) = &edu);
561 assert_eq!(content.sender, "@john:example.com");
562 assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
563 assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
564 assert!(content.messages.contains_key("@alice:example.org"));
565
566 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
567 }
568
569 #[test]
570 fn signing_key_update_edu() {
571 let json = json!({
572 "content": {
573 "master_key": {
574 "keys": {
575 "ed25519:alice+base64+public+key": "alice+base64+public+key",
576 "ed25519:base64+master+public+key": "base64+master+public+key"
577 },
578 "signatures": {
579 "@alice:example.com": {
580 "ed25519:alice+base64+master+key": "signature+of+key"
581 }
582 },
583 "usage": [
584 "master"
585 ],
586 "user_id": "@alice:example.com"
587 },
588 "self_signing_key": {
589 "keys": {
590 "ed25519:alice+base64+public+key": "alice+base64+public+key",
591 "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
592 },
593 "signatures": {
594 "@alice:example.com": {
595 "ed25519:alice+base64+master+key": "signature+of+key",
596 "ed25519:base64+master+public+key": "signature+of+self+signing+key"
597 }
598 },
599 "usage": [
600 "self_signing"
601 ],
602 "user_id": "@alice:example.com"
603 },
604 "user_id": "@alice:example.com"
605 },
606 "edu_type": "m.signing_key_update"
607 });
608
609 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
610 assert_let!(Edu::SigningKeyUpdate(content) = &edu);
611 assert_eq!(content.user_id, "@alice:example.com");
612 assert!(content.master_key.is_some());
613 assert!(content.self_signing_key.is_some());
614
615 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
616 }
617
618 #[test]
619 fn presence_edu() {
620 let json = json!({
621 "content": {
622 "push": [
623 {
624 "user_id": "@alice:example.com",
625 "presence": "online",
626 "currently_active": true,
627 "last_active_ago": 1000,
628 "status_msg": "Making cupcakes"
629 }
630 ]
631 },
632 "edu_type": "m.presence"
633 });
634
635 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
636 assert_let!(Edu::Presence(content) = &edu);
637 assert_eq!(content.push.len(), 1);
638 let presence_update = &content.push[0];
639 assert_eq!(presence_update.user_id, "@alice:example.com");
640 assert_eq!(presence_update.presence, PresenceState::Online);
641 assert!(presence_update.currently_active);
642 assert_eq!(presence_update.last_active_ago, uint!(1000));
643 assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
644 #[cfg(feature = "unstable-msc4495")]
645 {
646 assert!(presence_update.recipients.is_empty());
647 assert!(presence_update.stream_id.is_none());
648 assert!(presence_update.prev_id.is_none());
649 }
650
651 assert_to_canonical_json_eq!(edu, json);
652 }
653
654 #[cfg(feature = "unstable-msc4495")]
655 #[test]
656 fn msc4495_presence_edu() {
657 use js_int::int;
658
659 use crate::transactions::edu::PresenceRecipientListUpdates;
660
661 let json = json!({
662 "content": {
663 "push": [
664 {
665 "user_id": "@alice:example.com",
666 "presence": "online",
667 "currently_active": true,
668 "last_active_ago": 1000,
669 "status_msg": "Making cupcakes",
670 "stream_id": 321,
671 "prev_id": 123,
672 "recipients": {
673 "add": ["@bob:example.com"],
674 "delete": ["@charlie:example.com"]
675 }
676 }
677 ]
678 },
679 "edu_type": "m.presence"
680 });
681
682 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
683 assert_let!(Edu::Presence(content) = &edu);
684 assert_eq!(content.push.len(), 1);
685 let presence_update = &content.push[0];
686 assert_eq!(presence_update.user_id, "@alice:example.com");
687 assert_eq!(presence_update.presence, PresenceState::Online);
688 assert!(presence_update.currently_active);
689 assert_eq!(presence_update.last_active_ago, uint!(1000));
690 assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
691 assert_eq!(presence_update.stream_id, Some(int!(321)));
692 assert_eq!(presence_update.prev_id, Some(int!(123)));
693 assert_let!(PresenceRecipientListUpdates { add, delete } = &presence_update.recipients);
694 assert_eq!(add.len(), 1);
695 assert_eq!(delete.len(), 1);
696
697 assert_to_canonical_json_eq!(edu, json);
698 }
699}