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_matches2::assert_matches;
394 use js_int::uint;
395 use ruma_common::{
396 canonical_json::assert_to_canonical_json_eq, presence::PresenceState, room_id, user_id,
397 };
398 use ruma_events::ToDeviceEventType;
399 use serde_json::json;
400
401 use super::{DeviceListUpdateContent, Edu, ReceiptContent};
402
403 #[test]
404 fn device_list_update_edu() {
405 let json = json!({
406 "content": {
407 "deleted": false,
408 "device_display_name": "Mobile",
409 "device_id": "QBUAZIFURK",
410 "keys": {
411 "algorithms": [
412 "m.olm.v1.curve25519-aes-sha2",
413 "m.megolm.v1.aes-sha2"
414 ],
415 "device_id": "JLAFKJWSCS",
416 "keys": {
417 "curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
418 "ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
419 },
420 "signatures": {
421 "@alice:example.com": {
422 "ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
423 }
424 },
425 "user_id": "@alice:example.com"
426 },
427 "stream_id": 6,
428 "user_id": "@john:example.com"
429 },
430 "edu_type": "m.device_list_update"
431 });
432
433 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
434 assert_matches!(
435 &edu,
436 Edu::DeviceListUpdate(DeviceListUpdateContent {
437 user_id,
438 device_id,
439 device_display_name,
440 stream_id,
441 prev_id,
442 deleted,
443 keys,
444 })
445 );
446
447 assert_eq!(user_id, "@john:example.com");
448 assert_eq!(device_id, "QBUAZIFURK");
449 assert_eq!(device_display_name.as_deref(), Some("Mobile"));
450 assert_eq!(*stream_id, uint!(6));
451 assert_eq!(*prev_id, vec![]);
452 assert_eq!(*deleted, Some(false));
453 assert_matches!(keys, Some(_));
454
455 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
456 }
457
458 #[test]
459 fn minimal_device_list_update_edu() {
460 let json = json!({
461 "content": {
462 "device_id": "QBUAZIFURK",
463 "stream_id": 6,
464 "user_id": "@john:example.com"
465 },
466 "edu_type": "m.device_list_update"
467 });
468
469 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
470 assert_matches!(
471 &edu,
472 Edu::DeviceListUpdate(DeviceListUpdateContent {
473 user_id,
474 device_id,
475 device_display_name,
476 stream_id,
477 prev_id,
478 deleted,
479 keys,
480 })
481 );
482
483 assert_eq!(user_id, "@john:example.com");
484 assert_eq!(device_id, "QBUAZIFURK");
485 assert_eq!(*device_display_name, None);
486 assert_eq!(*stream_id, uint!(6));
487 assert_eq!(*prev_id, vec![]);
488 assert_eq!(*deleted, None);
489 assert_matches!(keys, None);
490
491 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
492 }
493
494 #[test]
495 fn receipt_edu() {
496 let json = json!({
497 "content": {
498 "!some_room:example.org": {
499 "m.read": {
500 "@john:matrix.org": {
501 "data": {
502 "ts": 1_533_358
503 },
504 "event_ids": [
505 "$read_this_event:matrix.org"
506 ]
507 }
508 }
509 }
510 },
511 "edu_type": "m.receipt"
512 });
513
514 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
515 assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
516 assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
517
518 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
519 }
520
521 #[test]
522 fn typing_edu() {
523 let json = json!({
524 "content": {
525 "room_id": "!somewhere:matrix.org",
526 "typing": true,
527 "user_id": "@john:matrix.org"
528 },
529 "edu_type": "m.typing"
530 });
531
532 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
533 assert_matches!(&edu, Edu::Typing(content));
534 assert_eq!(content.room_id, "!somewhere:matrix.org");
535 assert_eq!(content.user_id, "@john:matrix.org");
536 assert!(content.typing);
537
538 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
539 }
540
541 #[test]
542 fn direct_to_device_edu() {
543 let json = json!({
544 "content": {
545 "message_id": "hiezohf6Hoo7kaev",
546 "messages": {
547 "@alice:example.org": {
548 "IWHQUZUIAH": {
549 "algorithm": "m.megolm.v1.aes-sha2",
550 "room_id": "!Cuyf34gef24t:localhost",
551 "session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
552 "session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
553 }
554 }
555 },
556 "sender": "@john:example.com",
557 "type": "m.room_key_request"
558 },
559 "edu_type": "m.direct_to_device"
560 });
561
562 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
563 assert_matches!(&edu, Edu::DirectToDevice(content));
564 assert_eq!(content.sender, "@john:example.com");
565 assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
566 assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
567 assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
568
569 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
570 }
571
572 #[test]
573 fn signing_key_update_edu() {
574 let json = json!({
575 "content": {
576 "master_key": {
577 "keys": {
578 "ed25519:alice+base64+public+key": "alice+base64+public+key",
579 "ed25519:base64+master+public+key": "base64+master+public+key"
580 },
581 "signatures": {
582 "@alice:example.com": {
583 "ed25519:alice+base64+master+key": "signature+of+key"
584 }
585 },
586 "usage": [
587 "master"
588 ],
589 "user_id": "@alice:example.com"
590 },
591 "self_signing_key": {
592 "keys": {
593 "ed25519:alice+base64+public+key": "alice+base64+public+key",
594 "ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
595 },
596 "signatures": {
597 "@alice:example.com": {
598 "ed25519:alice+base64+master+key": "signature+of+key",
599 "ed25519:base64+master+public+key": "signature+of+self+signing+key"
600 }
601 },
602 "usage": [
603 "self_signing"
604 ],
605 "user_id": "@alice:example.com"
606 },
607 "user_id": "@alice:example.com"
608 },
609 "edu_type": "m.signing_key_update"
610 });
611
612 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
613 assert_matches!(&edu, Edu::SigningKeyUpdate(content));
614 assert_eq!(content.user_id, "@alice:example.com");
615 assert!(content.master_key.is_some());
616 assert!(content.self_signing_key.is_some());
617
618 assert_eq!(serde_json::to_value(&edu).unwrap(), json);
619 }
620
621 #[test]
622 fn presence_edu() {
623 let json = json!({
624 "content": {
625 "push": [
626 {
627 "user_id": "@alice:example.com",
628 "presence": "online",
629 "currently_active": true,
630 "last_active_ago": 1000,
631 "status_msg": "Making cupcakes"
632 }
633 ]
634 },
635 "edu_type": "m.presence"
636 });
637
638 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
639 assert_matches!(&edu, Edu::Presence(content));
640 assert_eq!(content.push.len(), 1);
641 let presence_update = &content.push[0];
642 assert_eq!(presence_update.user_id, "@alice:example.com");
643 assert_eq!(presence_update.presence, PresenceState::Online);
644 assert!(presence_update.currently_active);
645 assert_eq!(presence_update.last_active_ago, uint!(1000));
646 assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
647 #[cfg(feature = "unstable-msc4495")]
648 {
649 assert!(presence_update.recipients.is_empty());
650 assert!(presence_update.stream_id.is_none());
651 assert!(presence_update.prev_id.is_none());
652 }
653
654 assert_to_canonical_json_eq!(edu, json);
655 }
656
657 #[cfg(feature = "unstable-msc4495")]
658 #[test]
659 fn msc4495_presence_edu() {
660 use js_int::int;
661
662 use crate::transactions::edu::PresenceRecipientListUpdates;
663
664 let json = json!({
665 "content": {
666 "push": [
667 {
668 "user_id": "@alice:example.com",
669 "presence": "online",
670 "currently_active": true,
671 "last_active_ago": 1000,
672 "status_msg": "Making cupcakes",
673 "stream_id": 321,
674 "prev_id": 123,
675 "recipients": {
676 "add": ["@bob:example.com"],
677 "delete": ["@charlie:example.com"]
678 }
679 }
680 ]
681 },
682 "edu_type": "m.presence"
683 });
684
685 let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
686 assert_matches!(&edu, Edu::Presence(content));
687 assert_eq!(content.push.len(), 1);
688 let presence_update = &content.push[0];
689 assert_eq!(presence_update.user_id, "@alice:example.com");
690 assert_eq!(presence_update.presence, PresenceState::Online);
691 assert!(presence_update.currently_active);
692 assert_eq!(presence_update.last_active_ago, uint!(1000));
693 assert_eq!(presence_update.status_msg.as_deref(), Some("Making cupcakes"));
694 assert_eq!(presence_update.stream_id, Some(int!(321)));
695 assert_eq!(presence_update.prev_id, Some(int!(123)));
696 assert_matches!(&presence_update.recipients, PresenceRecipientListUpdates { add, delete });
697 assert_eq!(add.len(), 1);
698 assert_eq!(delete.len(), 1);
699
700 assert_to_canonical_json_eq!(edu, json);
701 }
702}