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