use std::collections::BTreeMap;
use js_int::UInt;
use ruma_common::{
encryption::{CrossSigningKey, DeviceKeys},
presence::PresenceState,
serde::{from_raw_json_value, Raw},
to_device::DeviceIdOrAllDevices,
OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedTransactionId, OwnedUserId,
};
use ruma_events::{receipt::Receipt, AnyToDeviceEventContent, ToDeviceEventType};
use serde::{de, Deserialize, Serialize};
use serde_json::{value::RawValue as RawJsonValue, Value as JsonValue};
#[derive(Clone, Debug, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
#[serde(tag = "edu_type", content = "content")]
pub enum Edu {
#[serde(rename = "m.presence")]
Presence(PresenceContent),
#[serde(rename = "m.receipt")]
Receipt(ReceiptContent),
#[serde(rename = "m.typing")]
Typing(TypingContent),
#[serde(rename = "m.device_list_update")]
DeviceListUpdate(DeviceListUpdateContent),
#[serde(rename = "m.direct_to_device")]
DirectToDevice(DirectDeviceContent),
#[serde(rename = "m.signing_key_update")]
SigningKeyUpdate(SigningKeyUpdateContent),
#[doc(hidden)]
_Custom(JsonValue),
}
#[derive(Debug, Deserialize)]
struct EduDeHelper {
edu_type: String,
content: Box<RawJsonValue>,
}
impl<'de> Deserialize<'de> for Edu {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: de::Deserializer<'de>,
{
let json = Box::<RawJsonValue>::deserialize(deserializer)?;
let EduDeHelper { edu_type, content } = from_raw_json_value(&json)?;
Ok(match edu_type.as_ref() {
"m.presence" => Self::Presence(from_raw_json_value(&content)?),
"m.receipt" => Self::Receipt(from_raw_json_value(&content)?),
"m.typing" => Self::Typing(from_raw_json_value(&content)?),
"m.device_list_update" => Self::DeviceListUpdate(from_raw_json_value(&content)?),
"m.direct_to_device" => Self::DirectToDevice(from_raw_json_value(&content)?),
"m.signing_key_update" => Self::SigningKeyUpdate(from_raw_json_value(&content)?),
_ => Self::_Custom(from_raw_json_value(&content)?),
})
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct PresenceContent {
pub push: Vec<PresenceUpdate>,
}
impl PresenceContent {
pub fn new(push: Vec<PresenceUpdate>) -> Self {
Self { push }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct PresenceUpdate {
pub user_id: OwnedUserId,
pub presence: PresenceState,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_msg: Option<String>,
pub last_active_ago: UInt,
#[serde(default)]
pub currently_active: bool,
}
impl PresenceUpdate {
pub fn new(user_id: OwnedUserId, presence: PresenceState, last_activity: UInt) -> Self {
Self {
user_id,
presence,
last_active_ago: last_activity,
status_msg: None,
currently_active: false,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ReceiptContent {
#[serde(flatten)]
pub receipts: BTreeMap<OwnedRoomId, ReceiptMap>,
}
impl ReceiptContent {
pub fn new(receipts: BTreeMap<OwnedRoomId, ReceiptMap>) -> Self {
Self { receipts }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ReceiptMap {
#[serde(rename = "m.read")]
pub read: BTreeMap<OwnedUserId, ReceiptData>,
}
impl ReceiptMap {
pub fn new(read: BTreeMap<OwnedUserId, ReceiptData>) -> Self {
Self { read }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct ReceiptData {
pub data: Receipt,
pub event_ids: Vec<OwnedEventId>,
}
impl ReceiptData {
pub fn new(data: Receipt, event_ids: Vec<OwnedEventId>) -> Self {
Self { data, event_ids }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct TypingContent {
pub room_id: OwnedRoomId,
pub user_id: OwnedUserId,
pub typing: bool,
}
impl TypingContent {
pub fn new(room_id: OwnedRoomId, user_id: OwnedUserId, typing: bool) -> Self {
Self { room_id, user_id, typing }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct DeviceListUpdateContent {
pub user_id: OwnedUserId,
pub device_id: OwnedDeviceId,
#[serde(skip_serializing_if = "Option::is_none")]
pub device_display_name: Option<String>,
pub stream_id: UInt,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub prev_id: Vec<UInt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deleted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub keys: Option<Raw<DeviceKeys>>,
}
impl DeviceListUpdateContent {
pub fn new(user_id: OwnedUserId, device_id: OwnedDeviceId, stream_id: UInt) -> Self {
Self {
user_id,
device_id,
device_display_name: None,
stream_id,
prev_id: vec![],
deleted: None,
keys: None,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct DirectDeviceContent {
pub sender: OwnedUserId,
#[serde(rename = "type")]
pub ev_type: ToDeviceEventType,
pub message_id: OwnedTransactionId,
pub messages: DirectDeviceMessages,
}
impl DirectDeviceContent {
pub fn new(
sender: OwnedUserId,
ev_type: ToDeviceEventType,
message_id: OwnedTransactionId,
) -> Self {
Self { sender, ev_type, message_id, messages: DirectDeviceMessages::new() }
}
}
pub type DirectDeviceMessages =
BTreeMap<OwnedUserId, BTreeMap<DeviceIdOrAllDevices, Raw<AnyToDeviceEventContent>>>;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SigningKeyUpdateContent {
pub user_id: OwnedUserId,
#[serde(skip_serializing_if = "Option::is_none")]
pub master_key: Option<Raw<CrossSigningKey>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub self_signing_key: Option<Raw<CrossSigningKey>>,
}
impl SigningKeyUpdateContent {
pub fn new(user_id: OwnedUserId) -> Self {
Self { user_id, master_key: None, self_signing_key: None }
}
}
#[cfg(test)]
mod tests {
use assert_matches2::assert_matches;
use js_int::uint;
use ruma_common::{room_id, user_id};
use ruma_events::ToDeviceEventType;
use serde_json::json;
use super::{DeviceListUpdateContent, Edu, ReceiptContent};
#[test]
fn device_list_update_edu() {
let json = json!({
"content": {
"deleted": false,
"device_display_name": "Mobile",
"device_id": "QBUAZIFURK",
"keys": {
"algorithms": [
"m.olm.v1.curve25519-aes-sha2",
"m.megolm.v1.aes-sha2"
],
"device_id": "JLAFKJWSCS",
"keys": {
"curve25519:JLAFKJWSCS": "3C5BFWi2Y8MaVvjM8M22DBmh24PmgR0nPvJOIArzgyI",
"ed25519:JLAFKJWSCS": "lEuiRJBit0IG6nUf5pUzWTUEsRVVe/HJkoKuEww9ULI"
},
"signatures": {
"@alice:example.com": {
"ed25519:JLAFKJWSCS": "dSO80A01XiigH3uBiDVx/EjzaoycHcjq9lfQX0uWsqxl2giMIiSPR8a4d291W1ihKJL/a+myXS367WT6NAIcBA"
}
},
"user_id": "@alice:example.com"
},
"stream_id": 6,
"user_id": "@john:example.com"
},
"edu_type": "m.device_list_update"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(
&edu,
Edu::DeviceListUpdate(DeviceListUpdateContent {
user_id,
device_id,
device_display_name,
stream_id,
prev_id,
deleted,
keys,
})
);
assert_eq!(user_id, "@john:example.com");
assert_eq!(device_id, "QBUAZIFURK");
assert_eq!(device_display_name.as_deref(), Some("Mobile"));
assert_eq!(*stream_id, uint!(6));
assert_eq!(*prev_id, vec![]);
assert_eq!(*deleted, Some(false));
assert_matches!(keys, Some(_));
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
#[test]
fn minimal_device_list_update_edu() {
let json = json!({
"content": {
"device_id": "QBUAZIFURK",
"stream_id": 6,
"user_id": "@john:example.com"
},
"edu_type": "m.device_list_update"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(
&edu,
Edu::DeviceListUpdate(DeviceListUpdateContent {
user_id,
device_id,
device_display_name,
stream_id,
prev_id,
deleted,
keys,
})
);
assert_eq!(user_id, "@john:example.com");
assert_eq!(device_id, "QBUAZIFURK");
assert_eq!(*device_display_name, None);
assert_eq!(*stream_id, uint!(6));
assert_eq!(*prev_id, vec![]);
assert_eq!(*deleted, None);
assert_matches!(keys, None);
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
#[test]
fn receipt_edu() {
let json = json!({
"content": {
"!some_room:example.org": {
"m.read": {
"@john:matrix.org": {
"data": {
"ts": 1_533_358
},
"event_ids": [
"$read_this_event:matrix.org"
]
}
}
}
},
"edu_type": "m.receipt"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(&edu, Edu::Receipt(ReceiptContent { receipts }));
assert!(receipts.get(room_id!("!some_room:example.org")).is_some());
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
#[test]
fn typing_edu() {
let json = json!({
"content": {
"room_id": "!somewhere:matrix.org",
"typing": true,
"user_id": "@john:matrix.org"
},
"edu_type": "m.typing"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(&edu, Edu::Typing(content));
assert_eq!(content.room_id, "!somewhere:matrix.org");
assert_eq!(content.user_id, "@john:matrix.org");
assert!(content.typing);
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
#[test]
fn direct_to_device_edu() {
let json = json!({
"content": {
"message_id": "hiezohf6Hoo7kaev",
"messages": {
"@alice:example.org": {
"IWHQUZUIAH": {
"algorithm": "m.megolm.v1.aes-sha2",
"room_id": "!Cuyf34gef24t:localhost",
"session_id": "X3lUlvLELLYxeTx4yOVu6UDpasGEVO0Jbu+QFnm0cKQ",
"session_key": "AgAAAADxKHa9uFxcXzwYoNueL5Xqi69IkD4sni8LlfJL7qNBEY..."
}
}
},
"sender": "@john:example.com",
"type": "m.room_key_request"
},
"edu_type": "m.direct_to_device"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(&edu, Edu::DirectToDevice(content));
assert_eq!(content.sender, "@john:example.com");
assert_eq!(content.ev_type, ToDeviceEventType::RoomKeyRequest);
assert_eq!(content.message_id, "hiezohf6Hoo7kaev");
assert!(content.messages.get(user_id!("@alice:example.org")).is_some());
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
#[test]
fn signing_key_update_edu() {
let json = json!({
"content": {
"master_key": {
"keys": {
"ed25519:alice+base64+public+key": "alice+base64+public+key",
"ed25519:base64+master+public+key": "base64+master+public+key"
},
"signatures": {
"@alice:example.com": {
"ed25519:alice+base64+master+key": "signature+of+key"
}
},
"usage": [
"master"
],
"user_id": "@alice:example.com"
},
"self_signing_key": {
"keys": {
"ed25519:alice+base64+public+key": "alice+base64+public+key",
"ed25519:base64+self+signing+public+key": "base64+self+signing+master+public+key"
},
"signatures": {
"@alice:example.com": {
"ed25519:alice+base64+master+key": "signature+of+key",
"ed25519:base64+master+public+key": "signature+of+self+signing+key"
}
},
"usage": [
"self_signing"
],
"user_id": "@alice:example.com"
},
"user_id": "@alice:example.com"
},
"edu_type": "m.signing_key_update"
});
let edu = serde_json::from_value::<Edu>(json.clone()).unwrap();
assert_matches!(&edu, Edu::SigningKeyUpdate(content));
assert_eq!(content.user_id, "@alice:example.com");
assert!(content.master_key.is_some());
assert!(content.self_signing_key.is_some());
assert_eq!(serde_json::to_value(&edu).unwrap(), json);
}
}