diff --git a/ruma-events/CHANGELOG.md b/ruma-events/CHANGELOG.md index a561e4f6..9fbf17dc 100644 --- a/ruma-events/CHANGELOG.md +++ b/ruma-events/CHANGELOG.md @@ -10,6 +10,7 @@ Breaking changes: struct variants * This change removes the types `EventMatchCondition`, `RoomMemberCountCondition` and `SenderNotificationPermissionCondition` +* Add PDU types: `pdu::{Pdu, PduStub}` Improvements: diff --git a/ruma-events/src/lib.rs b/ruma-events/src/lib.rs index 2b87bad2..91810ed7 100644 --- a/ruma-events/src/lib.rs +++ b/ruma-events/src/lib.rs @@ -147,6 +147,7 @@ pub mod forwarded_room_key; pub mod fully_read; pub mod ignored_user_list; pub mod key; +pub mod pdu; pub mod presence; pub mod push_rules; pub mod receipt; diff --git a/ruma-events/src/pdu.rs b/ruma-events/src/pdu.rs new file mode 100644 index 00000000..75aad814 --- /dev/null +++ b/ruma-events/src/pdu.rs @@ -0,0 +1,325 @@ +//! Types for persistent data unit schemas +//! +//! The differences between the `RoomV1Pdu` schema and the `RoomV3Pdu` schema are +//! that the `RoomV1Pdu` takes an `event_id` field (`RoomV3Pdu` does not), and +//! `auth_events` and `prev_events` take `Vec<(EventId, EventHash)> rather than +//! `Vec` in `RoomV3Pdu`. +//! +//! The stubbed versions of each PDU type remove the `event_id` field (if any) +//! and the `room_id` field for use in PDU templates. + +use std::{collections::BTreeMap, time::SystemTime}; + +use js_int::UInt; +use ruma_events::EventType; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +/// Enum for PDU schemas +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum Pdu { + /// PDU for room versions 1 and 2. + RoomV1Pdu(RoomV1Pdu), + /// PDU for room versions 3 and above. + RoomV3Pdu(RoomV3Pdu), +} + +/// A 'persistent data unit' (event) for room versions 1 and 2. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomV1Pdu { + /// Event ID for the PDU. + pub event_id: EventId, + + /// The room this event belongs to. + pub room_id: RoomId, + + /// The user id of the user who sent this event. + pub sender: UserId, + + /// The `server_name` of the homeserver that created this event. + pub origin: String, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Encode event type as content enum variant, like event enums do + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub prev_events: Vec<(EventId, EventHash)>, + + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub auth_events: Vec<(EventId, EventHash)>, + + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + + /// Content hashes of the PDU. + pub hashes: EventHash, + + /// Signatures for the PDU. + pub signatures: BTreeMap>, +} + +/// A 'persistent data unit' (event) for room versions 3 and beyond. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomV3Pdu { + /// The room this event belongs to. + pub room_id: RoomId, + + /// The user id of the user who sent this event. + pub sender: UserId, + + /// The `server_name` of the homeserver that created this event. + pub origin: String, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Encode event type as content enum variant, like event enums do + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + pub prev_events: Vec, + + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + pub auth_events: Vec, + + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + + /// Content hashes of the PDU. + pub hashes: EventHash, + + /// Signatures for the PDU. + pub signatures: BTreeMap>, +} + +/// PDU type without event and room IDs. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PduStub { + /// Stub for PDUs of room version 1 and 2. + RoomV1PduStub(RoomV1PduStub), + + /// Stub for PDUs of room versions 3 and above. + RoomV3PduStub(RoomV3PduStub), +} + +impl PduStub { + /// Helper method to get PDU from a PDU stub. + pub fn into_pdu(self, room_id: RoomId, event_id: EventId) -> Pdu { + match self { + PduStub::RoomV1PduStub(v1_stub) => { + Pdu::RoomV1Pdu(v1_stub.into_v1_pdu(room_id, event_id)) + } + PduStub::RoomV3PduStub(v3_stub) => Pdu::RoomV3Pdu(v3_stub.into_v3_pdu(room_id)), + } + } +} + +/// Stub for PDUs of room version 1 and 2. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomV1PduStub { + /// The user id of the user who sent this event. + pub sender: UserId, + + /// The `server_name` of the homeserver that created this event. + pub origin: String, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Encode event type as content enum variant, like event enums do + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + pub prev_events: Vec<(EventId, EventHash)>, + + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + pub auth_events: Vec<(EventId, EventHash)>, + + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + + /// Content hashes of the PDU. + pub hashes: EventHash, + + /// Signatures for the PDU. + pub signatures: BTreeMap>, +} + +impl RoomV1PduStub { + /// Converts a V1 PDU stub into a full V1 PDU. + pub fn into_v1_pdu(self, room_id: RoomId, event_id: EventId) -> RoomV1Pdu { + RoomV1Pdu { + event_id, + room_id, + sender: self.sender, + origin: self.origin, + origin_server_ts: self.origin_server_ts, + kind: self.kind, + content: self.content, + state_key: self.state_key, + prev_events: self.prev_events, + depth: self.depth, + auth_events: self.auth_events, + redacts: self.redacts, + unsigned: self.unsigned, + hashes: self.hashes, + signatures: self.signatures, + } + } +} + +/// Stub for PDUs of room versions 3 and above. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomV3PduStub { + /// The user id of the user who sent this event. + pub sender: UserId, + + /// The `server_name` of the homeserver that created this event. + pub origin: String, + + /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver + /// of when this event was created. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + // TODO: Encode event type as content enum variant, like event enums do + /// The event's type. + #[serde(rename = "type")] + pub kind: EventType, + + /// The event's content. + pub content: JsonValue, + + /// A key that determines which piece of room state the event represents. + #[serde(skip_serializing_if = "Option::is_none")] + pub state_key: Option, + + /// Event IDs for the most recent events in the room that the homeserver was + /// aware of when it created this event. + pub prev_events: Vec, + + /// The maximum depth of the `prev_events`, plus one. + pub depth: UInt, + + /// Event IDs for the authorization events that would allow this event to be + /// in the room. + pub auth_events: Vec, + + /// For redaction events, the ID of the event being redacted. + #[serde(skip_serializing_if = "Option::is_none")] + pub redacts: Option, + + /// Additional data added by the origin server but not covered by the + /// signatures. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub unsigned: BTreeMap, + + /// Content hashes of the PDU. + pub hashes: EventHash, + + /// Signatures for the PDU. + pub signatures: BTreeMap>, +} + +impl RoomV3PduStub { + /// Converts a V3 PDU stub into a full V3 PDU. + pub fn into_v3_pdu(self, room_id: RoomId) -> RoomV3Pdu { + RoomV3Pdu { + room_id, + sender: self.sender, + origin: self.origin, + origin_server_ts: self.origin_server_ts, + kind: self.kind, + content: self.content, + state_key: self.state_key, + prev_events: self.prev_events, + depth: self.depth, + auth_events: self.auth_events, + redacts: self.redacts, + unsigned: self.unsigned, + hashes: self.hashes, + signatures: self.signatures, + } + } +} + +/// Content hashes of a PDU. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct EventHash { + /// The SHA-256 hash. + pub sha256: String, +} diff --git a/ruma-events/tests/pdu.rs b/ruma-events/tests/pdu.rs new file mode 100644 index 00000000..508c3de3 --- /dev/null +++ b/ruma-events/tests/pdu.rs @@ -0,0 +1,605 @@ +use std::{ + collections::BTreeMap, + convert::TryFrom, + time::{Duration, SystemTime}, +}; + +use matches::assert_matches; +use ruma_events::{ + pdu::{EventHash, Pdu, PduStub, RoomV1Pdu, RoomV1PduStub, RoomV3Pdu, RoomV3PduStub}, + EventType, +}; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde_json::{from_value as from_json_value, json, to_value as to_json_value}; + +#[test] +fn serialize_stub_as_v1() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v1_stub = RoomV1PduStub { + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![( + EventId::try_from("$previousevent:matrix.org").unwrap(), + EventHash { sha256: "123567".to_string() }, + )], + depth: 2_u32.into(), + auth_events: vec![( + EventId::try_from("$someauthevent:matrix.org").unwrap(), + EventHash { sha256: "21389CFEDABC".to_string() }, + )], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned, + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures, + }; + let pdu_stub = PduStub::RoomV1PduStub(v1_stub); + let json = json!({ + "sender": "@sender:example.com", + "origin": "matrix.org", + "origin_server_ts": 1_592_050_773_658 as usize, + "type": "m.room.power_levels", + "content": { + "testing": 123 + }, + "state_key": "state", + "prev_events": [ + [ "$previousevent:matrix.org", {"sha256": "123567"} ] + ], + "depth": 2, + "auth_events": [ + ["$someauthevent:matrix.org", {"sha256": "21389CFEDABC"}] + ], + "redacts": "$9654:matrix.org", + "unsigned": { + "somekey": { "a": 456 } }, + "hashes": { "sha256": "1233543bABACDEF" }, + "signatures": { + "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } + } + }); + + assert_eq!(to_json_value(&pdu_stub).unwrap(), json); +} + +#[test] +fn serialize_stub_as_v3() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v3_stub = RoomV3PduStub { + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![EventId::try_from("$previousevent:matrix.org").unwrap()], + depth: 2_u32.into(), + auth_events: vec![EventId::try_from("$someauthevent:matrix.org").unwrap()], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned, + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures, + }; + let pdu_stub = PduStub::RoomV3PduStub(v3_stub); + let json = json!({ + "sender": "@sender:example.com", + "origin": "matrix.org", + "origin_server_ts": 1_592_050_773_658 as usize, + "type": "m.room.power_levels", + "content": { + "testing": 123 + }, + "state_key": "state", + "prev_events": [ "$previousevent:matrix.org" ], + "depth": 2, + "auth_events": ["$someauthevent:matrix.org" ], + "redacts": "$9654:matrix.org", + "unsigned": { + "somekey": { "a": 456 } }, + "hashes": { "sha256": "1233543bABACDEF" }, + "signatures": { + "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } + } + }); + + assert_eq!(to_json_value(&pdu_stub).unwrap(), json); +} + +#[test] +fn test_deserialize_stub_as_v1() { + let json = json!({ + "auth_events": [ + [ + "$abc123:matrix.org", + { + "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" + } + ] + ], + "content": { + "key": "value" + }, + "depth": 12, + "event_id": "$a4ecee13e2accdadf56c1025:example.com", + "hashes": { + "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" + }, + "origin": "matrix.org", + "origin_server_ts": 1_234_567_890, + "prev_events": [ + [ + "$abc123:matrix.org", + { + "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" + } + ] + ], + "redacts": "$def456:matrix.org", + "room_id": "!abc123:matrix.org", + "sender": "@someone:matrix.org", + "signatures": { + "example.com": { + "ed25519:key_version:": "86BytesOfSignatureOfTheRedactedEvent" + } + }, + "state_key": "my_key", + "type": "m.room.message", + "unsigned": { + "key": "value" + } + }); + let parsed = from_json_value::(json).unwrap(); + + match parsed { + PduStub::RoomV1PduStub(v1_stub) => { + assert_eq!( + v1_stub.auth_events.first().unwrap().0, + EventId::try_from("$abc123:matrix.org").unwrap() + ); + assert_eq!( + v1_stub.auth_events.first().unwrap().1.sha256, + "Base64EncodedSha256HashesShouldBe43BytesLong" + ); + } + PduStub::RoomV3PduStub(_) => panic!("Matched V3 stub"), + } +} + +#[test] +fn deserialize_stub_as_v3() { + let json = json!({ + "auth_events": [ + "$abc123:matrix.org" + ], + "content": { + "key": "value" + }, + "depth": 12, + "event_id": "$a4ecee13e2accdadf56c1025:example.com", + "hashes": { + "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" + }, + "origin": "matrix.org", + "origin_server_ts": 1_234_567_890, + "prev_events": [ + "$abc123:matrix.org" + ], + "redacts": "$def456:matrix.org", + "room_id": "!abc123:matrix.org", + "sender": "@someone:matrix.org", + "signatures": { + "example.com": { + "ed25519:key_version:": "86BytesOfSignatureOfTheRedactedEvent" + } + }, + "state_key": "my_key", + "type": "m.room.message", + "unsigned": { + "key": "value" + } + }); + let parsed = from_json_value::(json).unwrap(); + + match parsed { + PduStub::RoomV1PduStub(_) => panic!("Matched V1 stub"), + PduStub::RoomV3PduStub(v3_stub) => { + assert_eq!( + v3_stub.auth_events.first().unwrap(), + &EventId::try_from("$abc123:matrix.org").unwrap() + ); + } + } +} + +#[test] +fn serialize_pdu_as_v1() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v1_pdu = RoomV1Pdu { + room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(), + event_id: EventId::try_from("$somejoinevent:matrix.org").unwrap(), + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![( + EventId::try_from("$previousevent:matrix.org").unwrap(), + EventHash { sha256: "123567".to_string() }, + )], + depth: 2_u32.into(), + auth_events: vec![( + EventId::try_from("$someauthevent:matrix.org").unwrap(), + EventHash { sha256: "21389CFEDABC".to_string() }, + )], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned, + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures, + }; + let pdu = Pdu::RoomV1Pdu(v1_pdu); + let json = json!({ + "room_id": "!n8f893n9:example.com", + "event_id": "$somejoinevent:matrix.org", + "sender": "@sender:example.com", + "origin": "matrix.org", + "origin_server_ts": 1_592_050_773_658 as usize, + "type": "m.room.power_levels", + "content": { + "testing": 123 + }, + "state_key": "state", + "prev_events": [ + [ "$previousevent:matrix.org", {"sha256": "123567"} ] + ], + "depth": 2, + "auth_events": [ + ["$someauthevent:matrix.org", {"sha256": "21389CFEDABC"}] + ], + "redacts": "$9654:matrix.org", + "unsigned": { + "somekey": { "a": 456 } }, + "hashes": { "sha256": "1233543bABACDEF" }, + "signatures": { + "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } + } + }); + + assert_eq!(to_json_value(&pdu).unwrap(), json); +} + +#[test] +fn serialize_pdu_as_v3() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v3_pdu = RoomV3Pdu { + room_id: RoomId::try_from("!n8f893n9:example.com").unwrap(), + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![EventId::try_from("$previousevent:matrix.org").unwrap()], + depth: 2_u32.into(), + auth_events: vec![EventId::try_from("$someauthevent:matrix.org").unwrap()], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned, + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures, + }; + let pdu_stub = Pdu::RoomV3Pdu(v3_pdu); + let json = json!({ + "room_id": "!n8f893n9:example.com", + "sender": "@sender:example.com", + "origin": "matrix.org", + "origin_server_ts": 1_592_050_773_658 as usize, + "type": "m.room.power_levels", + "content": { + "testing": 123 + }, + "state_key": "state", + "prev_events": [ "$previousevent:matrix.org" ], + "depth": 2, + "auth_events": ["$someauthevent:matrix.org" ], + "redacts": "$9654:matrix.org", + "unsigned": { + "somekey": { "a": 456 } }, + "hashes": { "sha256": "1233543bABACDEF" }, + "signatures": { + "example.com": { "ed25519:key_version":"86BytesOfSignatureOfTheRedactedEvent" } + } + }); + + assert_eq!(to_json_value(&pdu_stub).unwrap(), json); +} + +#[test] +fn test_deserialize_pdu_as_v1() { + let json = json!({ + "room_id": "!n8f893n9:example.com", + "event_id": "$somejoinevent:matrix.org", + "auth_events": [ + [ + "$abc123:matrix.org", + { + "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" + } + ] + ], + "content": { + "key": "value" + }, + "depth": 12, + "event_id": "$a4ecee13e2accdadf56c1025:example.com", + "hashes": { + "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" + }, + "origin": "matrix.org", + "origin_server_ts": 1_234_567_890, + "prev_events": [ + [ + "$abc123:matrix.org", + { + "sha256": "Base64EncodedSha256HashesShouldBe43BytesLong" + } + ] + ], + "redacts": "$def456:matrix.org", + "room_id": "!abc123:matrix.org", + "sender": "@someone:matrix.org", + "signatures": { + "example.com": { + "ed25519:key_version:": "86BytesOfSignatureOfTheRedactedEvent" + } + }, + "state_key": "my_key", + "type": "m.room.message", + "unsigned": { + "key": "value" + } + }); + let parsed = from_json_value::(json).unwrap(); + + match parsed { + Pdu::RoomV1Pdu(v1_pdu) => { + assert_eq!( + v1_pdu.auth_events.first().unwrap().0, + EventId::try_from("$abc123:matrix.org").unwrap() + ); + assert_eq!( + v1_pdu.auth_events.first().unwrap().1.sha256, + "Base64EncodedSha256HashesShouldBe43BytesLong" + ); + } + Pdu::RoomV3Pdu(_) => panic!("Matched V3 PDU"), + } +} + +#[test] +fn deserialize_pdu_as_v3() { + let json = json!({ + "room_id": "!n8f893n9:example.com", + "auth_events": [ + "$abc123:matrix.org" + ], + "content": { + "key": "value" + }, + "depth": 12, + "event_id": "$a4ecee13e2accdadf56c1025:example.com", + "hashes": { + "sha256": "ThisHashCoversAllFieldsInCaseThisIsRedacted" + }, + "origin": "matrix.org", + "origin_server_ts": 1_234_567_890, + "prev_events": [ + "$abc123:matrix.org" + ], + "redacts": "$def456:matrix.org", + "room_id": "!abc123:matrix.org", + "sender": "@someone:matrix.org", + "signatures": { + "example.com": { + "ed25519:key_version:": "86BytesOfSignatureOfTheRedactedEvent" + } + }, + "state_key": "my_key", + "type": "m.room.message", + "unsigned": { + "key": "value" + } + }); + let parsed = from_json_value::(json).unwrap(); + + match parsed { + Pdu::RoomV1Pdu(_) => panic!("Matched V1 PDU"), + Pdu::RoomV3Pdu(v3_pdu) => { + assert_eq!( + v3_pdu.auth_events.first().unwrap(), + &EventId::try_from("$abc123:matrix.org").unwrap() + ); + } + } +} + +#[test] +fn convert_v1_stub_to_pdu() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v1_stub = RoomV1PduStub { + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![( + EventId::try_from("$previousevent:matrix.org").unwrap(), + EventHash { sha256: "123567".to_string() }, + )], + depth: 2_u32.into(), + auth_events: vec![( + EventId::try_from("$someauthevent:matrix.org").unwrap(), + EventHash { sha256: "21389CFEDABC".to_string() }, + )], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned: (&unsigned).clone(), + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures: (&signatures).clone(), + }; + + assert_matches!( + v1_stub.into_v1_pdu( + RoomId::try_from("!n8f893n9:example.com").unwrap(), + EventId::try_from("$somejoinevent:matrix.org").unwrap() + ), + RoomV1Pdu { + room_id, + event_id, + sender, + origin, + origin_server_ts, + kind, + content, + state_key, + prev_events, + depth, + auth_events, + redacts, + unsigned, + hashes: EventHash { sha256 }, + signatures, + } if room_id == RoomId::try_from("!n8f893n9:example.com").unwrap() + && event_id == EventId::try_from("$somejoinevent:matrix.org").unwrap() + && sender == UserId::try_from("@sender:example.com").unwrap() + && origin == "matrix.org" + && origin_server_ts == SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658) + && kind == EventType::RoomPowerLevels + && content == json!({"testing": 123}) + && state_key == Some("state".to_string()) + && prev_events[0].0 == EventId::try_from("$previousevent:matrix.org").unwrap() + && prev_events[0].1.sha256 == "123567" + && depth == 2_u32.into() + && auth_events.first().unwrap().0 == EventId::try_from("$someauthevent:matrix.org").unwrap() + && auth_events.first().unwrap().1.sha256 == "21389CFEDABC" + && redacts == Some(EventId::try_from("$9654:matrix.org").unwrap()) + && unsigned == (&unsigned).clone() + && sha256 == "1233543bABACDEF" + && signatures == (&signatures).clone() + ); +} + +#[test] +fn convert_v3_stub_to_pdu() { + let mut signatures = BTreeMap::new(); + let mut inner_signature = BTreeMap::new(); + inner_signature.insert( + "ed25519:key_version".to_string(), + "86BytesOfSignatureOfTheRedactedEvent".to_string(), + ); + signatures.insert("example.com".to_string(), inner_signature); + + let mut unsigned = BTreeMap::new(); + unsigned.insert("somekey".to_string(), json!({"a": 456})); + + let v3_stub = RoomV3PduStub { + sender: UserId::try_from("@sender:example.com").unwrap(), + origin: "matrix.org".to_string(), + origin_server_ts: SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658), + kind: EventType::RoomPowerLevels, + content: json!({"testing": 123}), + state_key: Some("state".to_string()), + prev_events: vec![EventId::try_from("$previousevent:matrix.org").unwrap()], + depth: 2_u32.into(), + auth_events: vec![EventId::try_from("$someauthevent:matrix.org").unwrap()], + redacts: Some(EventId::try_from("$9654:matrix.org").unwrap()), + unsigned: (&unsigned).clone(), + hashes: EventHash { sha256: "1233543bABACDEF".to_string() }, + signatures: (&signatures).clone(), + }; + + assert_matches!( + v3_stub.into_v3_pdu(RoomId::try_from("!n8f893n9:example.com").unwrap()), + RoomV3Pdu { + room_id, + sender, + origin, + origin_server_ts, + kind, + content, + state_key, + prev_events, + depth, + auth_events, + redacts, + unsigned, + hashes: EventHash { sha256 }, + signatures, + } if room_id == RoomId::try_from("!n8f893n9:example.com").unwrap() + && sender == UserId::try_from("@sender:example.com").unwrap() + && origin == "matrix.org" + && origin_server_ts == SystemTime::UNIX_EPOCH + Duration::from_millis(1_592_050_773_658) + && kind == EventType::RoomPowerLevels + && content == json!({"testing": 123}) + && state_key == Some("state".to_string()) + && prev_events == vec![EventId::try_from("$previousevent:matrix.org").unwrap()] + && depth == 2_u32.into() + && auth_events == vec![EventId::try_from("$someauthevent:matrix.org").unwrap()] + && redacts == Some(EventId::try_from("$9654:matrix.org").unwrap()) + && unsigned == (&unsigned).clone() + && sha256 == "1233543bABACDEF" + && signatures == (&signatures).clone() + ); +} diff --git a/ruma-federation-api/CHANGELOG.md b/ruma-federation-api/CHANGELOG.md index bed78bb5..581a742a 100644 --- a/ruma-federation-api/CHANGELOG.md +++ b/ruma-federation-api/CHANGELOG.md @@ -1,5 +1,9 @@ # [unreleased] +Breaking Changes: + +* Replace `RoomV3Pdu` with `ruma_events::pdu::{Pdu, PduStub}`. + Improvements: * Add endpoints: @@ -15,6 +19,7 @@ Improvements: create_join_event_template::v1 }, query::get_room_information::v1, + transactions::send_transaction_message::v1, version::get_server_version::v1 ``` diff --git a/ruma-federation-api/src/lib.rs b/ruma-federation-api/src/lib.rs index ef6a1c66..ef95d3f6 100644 --- a/ruma-federation-api/src/lib.rs +++ b/ruma-federation-api/src/lib.rs @@ -2,69 +2,10 @@ #![warn(missing_docs)] -use std::{collections::BTreeMap, time::SystemTime}; - -use ::serde::{Deserialize, Serialize}; -use js_int::UInt; -use ruma_events::EventType; -use ruma_identifiers::{EventId, RoomId, UserId}; -use serde_json::Value as JsonValue; - mod serde; pub mod directory; pub mod discovery; pub mod membership; pub mod query; - -/// A 'persistent data unit' (event) for room versions 3 and beyond. -#[derive(Deserialize, Serialize)] -pub struct RoomV3Pdu { - /// The room this event belongs to. - pub room_id: RoomId, - /// The user id of the user who sent this event. - pub sender: UserId, - /// The `server_name` of the homeserver that created this event. - pub origin: String, - /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver - /// of when this event was created. - #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] - pub origin_server_ts: SystemTime, - - // TODO: Replace with event content collection from ruma-events once that exists - /// The event's type. - #[serde(rename = "type")] - pub kind: EventType, - /// The event's content. - pub content: JsonValue, - - /// A key that determines which piece of room state the event represents. - #[serde(skip_serializing_if = "Option::is_none")] - pub state_key: Option, - /// Event IDs for the most recent events in the room that the homeserver was - /// aware of when it created this event. - pub prev_events: Vec, - /// The maximum depth of the `prev_events`, plus one. - pub depth: UInt, - /// Event IDs for the authorization events that would allow this event to be - /// in the room. - pub auth_events: Vec, - /// For redaction events, the ID of the event being redacted. - #[serde(skip_serializing_if = "Option::is_none")] - pub redacts: Option, - /// Additional data added by the origin server but not covered by the - /// signatures. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, - /// Content hashes of the PDU. - pub hashes: EventHash, - /// Signatures for the PDU. - pub signatures: BTreeMap>, -} - -/// Content hashes of a PDU. -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct EventHash { - /// The SHA-256 hash. - pub sha256: String, -} +pub mod transactions; diff --git a/ruma-federation-api/src/membership/create_join_event/mod.rs b/ruma-federation-api/src/membership/create_join_event/mod.rs index 4c6d2730..4e965454 100644 --- a/ruma-federation-api/src/membership/create_join_event/mod.rs +++ b/ruma-federation-api/src/membership/create_join_event/mod.rs @@ -2,11 +2,9 @@ pub mod v1; -use ruma_events::EventJson; +use ruma_events::{pdu::Pdu, EventJson}; use serde::{Deserialize, Serialize}; -use crate::RoomV3Pdu; - /// Full state of the room. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct RoomState { @@ -14,7 +12,7 @@ pub struct RoomState { pub origin: String, /// The full set of authorization events that make up the state of the room, /// and their authorization events, recursively. - pub auth_chain: Vec>, + pub auth_chain: Vec>, /// The room state. - pub state: Vec>, + pub state: Vec>, } diff --git a/ruma-federation-api/src/membership/create_join_event/v1.rs b/ruma-federation-api/src/membership/create_join_event/v1.rs index e4403f04..499ce3e9 100644 --- a/ruma-federation-api/src/membership/create_join_event/v1.rs +++ b/ruma-federation-api/src/membership/create_join_event/v1.rs @@ -1,15 +1,10 @@ //! [PUT /_matrix/federation/v1/send_join/{roomId}/{eventId}](https://matrix.org/docs/spec/server_server/r0.1.3#put-matrix-federation-v1-send-join-roomid-eventid) -use std::{collections::BTreeMap, time::SystemTime}; - -use js_int::UInt; use ruma_api::ruma_api; -use ruma_events::EventType; -use ruma_identifiers::{EventId, RoomId, UserId}; -use serde_json::Value as JsonValue; +use ruma_events::pdu::PduStub; +use ruma_identifiers::{EventId, RoomId}; use super::RoomState; -use crate::{EventHash, RoomV3Pdu}; ruma_api! { metadata { @@ -29,44 +24,9 @@ ruma_api! { #[ruma_api(path)] pub event_id: EventId, - /// The user id of the user who sent this event. - pub sender: UserId, - /// The `server_name` of the homeserver that created this event. - pub origin: String, - /// Timestamp (milliseconds since the UNIX epoch) on originating homeserver - /// of when this event was created. - #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] - pub origin_server_ts: SystemTime, - - // TODO: Replace with event content collection from ruma-events once that exists - /// The event's type. - #[serde(rename = "type")] - pub kind: EventType, - /// The event's content. - pub content: JsonValue, - - /// A key that determines which piece of room state the event represents. - #[serde(skip_serializing_if = "Option::is_none")] - pub state_key: Option, - /// Event IDs for the most recent events in the room that the homeserver was - /// aware of when it created this event. - pub prev_events: Vec, - /// The maximum depth of the `prev_events`, plus one. - pub depth: UInt, - /// Event IDs for the authorization events that would allow this event to be - /// in the room. - pub auth_events: Vec, - /// For redaction events, the ID of the event being redacted. - #[serde(skip_serializing_if = "Option::is_none")] - pub redacts: Option, - /// Additional data added by the origin server but not covered by the - /// signatures. - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub unsigned: BTreeMap, - /// Content hashes of the PDU. - pub hashes: EventHash, - /// Signatures for the PDU. - pub signatures: BTreeMap>, + /// PDU type without event and room IDs. + #[ruma_api(body)] + pub pdu_stub: PduStub, } response { @@ -76,29 +36,3 @@ ruma_api! { pub room_state: RoomState, } } - -impl Request { - /// Helper method to get event ID and PDU (with room ID) from the request - /// parameters. - pub fn into_id_and_v3_pdu(self) -> (EventId, RoomV3Pdu) { - ( - self.event_id, - RoomV3Pdu { - room_id: self.room_id, - sender: self.sender, - origin: self.origin, - origin_server_ts: self.origin_server_ts, - kind: self.kind, - content: self.content, - state_key: self.state_key, - prev_events: self.prev_events, - depth: self.depth, - auth_events: self.auth_events, - redacts: self.redacts, - unsigned: self.unsigned, - hashes: self.hashes, - signatures: self.signatures, - }, - ) - } -} diff --git a/ruma-federation-api/src/membership/create_join_event_template/v1.rs b/ruma-federation-api/src/membership/create_join_event_template/v1.rs index a25af1fd..84f88699 100644 --- a/ruma-federation-api/src/membership/create_join_event_template/v1.rs +++ b/ruma-federation-api/src/membership/create_join_event_template/v1.rs @@ -2,11 +2,9 @@ use js_int::UInt; use ruma_api::ruma_api; -use ruma_events::EventJson; +use ruma_events::{pdu::Pdu, EventJson}; use ruma_identifiers::{RoomId, UserId}; -use crate::RoomV3Pdu; - ruma_api! { metadata { description: "Send a request for a join event template to a resident server.", @@ -34,6 +32,6 @@ ruma_api! { /// The version of the room where the server is trying to join. pub room_version: Option, /// An unsigned template event. - pub event: EventJson, + pub event: EventJson, } } diff --git a/ruma-federation-api/src/serde.rs b/ruma-federation-api/src/serde.rs index 3808d8c5..9054a487 100644 --- a/ruma-federation-api/src/serde.rs +++ b/ruma-federation-api/src/serde.rs @@ -1,3 +1,4 @@ //! Modules for custom serde de/-serialization implementations. +pub mod pdu_process_response; pub mod room_state; diff --git a/ruma-federation-api/src/serde/pdu_process_response.rs b/ruma-federation-api/src/serde/pdu_process_response.rs new file mode 100644 index 00000000..57c0a807 --- /dev/null +++ b/ruma-federation-api/src/serde/pdu_process_response.rs @@ -0,0 +1,156 @@ +use std::{collections::BTreeMap, fmt}; + +use ruma_identifiers::EventId; +use serde::{ + de::{Deserializer, MapAccess, Visitor}, + ser::{SerializeMap, Serializer}, + Deserialize, Serialize, +}; + +pub fn serialize( + response: &BTreeMap>, + serializer: S, +) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(response.len()))?; + for (key, value) in response { + let wrapped_error = WrappedError { + error: match value { + Ok(_) => None, + Err(error) => Some(error.clone()), + }, + }; + map.serialize_entry(&key, &wrapped_error)?; + } + map.end() +} + +pub fn deserialize<'de, D>( + deserializer: D, +) -> Result>, D::Error> +where + D: Deserializer<'de>, +{ + deserializer.deserialize_map(PduProcessResponseVisitor) +} + +#[derive(Deserialize, Serialize)] +struct WrappedError { + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +struct PduProcessResponseVisitor; + +impl<'de> Visitor<'de> for PduProcessResponseVisitor { + type Value = BTreeMap>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("A map of EventIds to a map of optional errors") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = BTreeMap::new(); + + while let Some((key, value)) = access.next_entry::()? { + let v = match value.error { + None => Ok(()), + Some(error) => Err(error), + }; + map.insert(key, v); + } + Ok(map) + } +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, convert::TryFrom}; + + use ruma_identifiers::EventId; + use serde_json::{json, value::Serializer as JsonSerializer}; + + use super::{deserialize, serialize}; + + #[test] + fn serialize_error() { + let mut response: BTreeMap> = BTreeMap::new(); + response.insert( + EventId::try_from("$someevent:matrix.org").unwrap(), + Err("Some processing error.".into()), + ); + + let serialized = serialize(&response, JsonSerializer).unwrap(); + let json = json!({ + "$someevent:matrix.org": { "error": "Some processing error." } + }); + assert_eq!(serialized, json); + } + + #[test] + fn serialize_ok() { + let mut response: BTreeMap> = BTreeMap::new(); + response.insert(EventId::try_from("$someevent:matrix.org").unwrap(), Ok(())); + + let serialized = serialize(&response, serde_json::value::Serializer).unwrap(); + let json = json!({ + "$someevent:matrix.org": {} + }); + assert_eq!(serialized, json); + } + + #[test] + fn deserialize_error() { + let json = json!({ + "$someevent:matrix.org": { "error": "Some processing error." } + }); + + let response = deserialize(json).unwrap(); + let event_id = EventId::try_from("$someevent:matrix.org").unwrap(); + + let event_response = response.get(&event_id).unwrap().clone().unwrap_err(); + assert_eq!(event_response, "Some processing error."); + } + + #[test] + fn deserialize_null_error_is_ok() { + let json = json!({ + "$someevent:matrix.org": { "error": null } + }); + + let response = deserialize(json).unwrap(); + let event_id = EventId::try_from("$someevent:matrix.org").unwrap(); + + assert!(response.get(&event_id).unwrap().is_ok()); + } + + #[test] + fn desieralize_empty_error_is_err() { + let json = json!({ + "$someevent:matrix.org": { "error": "" } + }); + + let response = deserialize(json).unwrap(); + let event_id = EventId::try_from("$someevent:matrix.org").unwrap(); + + let event_response = response.get(&event_id).unwrap().clone().unwrap_err(); + assert_eq!(event_response, ""); + } + + #[test] + fn deserialize_ok() { + let json = json!({ + "$someevent:matrix.org": {} + }); + let response = deserialize(json).unwrap(); + assert!(response + .get(&EventId::try_from("$someevent:matrix.org").unwrap()) + .unwrap() + .is_ok()); + } +} diff --git a/ruma-federation-api/src/transactions.rs b/ruma-federation-api/src/transactions.rs new file mode 100644 index 00000000..e889b1f5 --- /dev/null +++ b/ruma-federation-api/src/transactions.rs @@ -0,0 +1,3 @@ +//! Endpoints for exchanging transaction messages between homeservers. + +pub mod send_transaction_message; diff --git a/ruma-federation-api/src/transactions/send_transaction_message/mod.rs b/ruma-federation-api/src/transactions/send_transaction_message/mod.rs new file mode 100644 index 00000000..135bfb5f --- /dev/null +++ b/ruma-federation-api/src/transactions/send_transaction_message/mod.rs @@ -0,0 +1,3 @@ +//! Endpoint to send live activity messages to another server. + +pub mod v1; diff --git a/ruma-federation-api/src/transactions/send_transaction_message/v1.rs b/ruma-federation-api/src/transactions/send_transaction_message/v1.rs new file mode 100644 index 00000000..0bc8f805 --- /dev/null +++ b/ruma-federation-api/src/transactions/send_transaction_message/v1.rs @@ -0,0 +1,59 @@ +//! [PUT /_matrix/federation/v1/send/{txnId}](https://matrix.org/docs/spec/server_server/r0.1.3#put-matrix-federation-v1-send-txnid) + +use std::{collections::BTreeMap, time::SystemTime}; + +use ruma_api::ruma_api; +use ruma_events::pdu::Pdu; +use ruma_identifiers::EventId; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +ruma_api! { + metadata { + description: "Send transaction messages to another server", + name: "send_transaction_message", + method: PUT, + path: "/_matrix/federation/v1/send/:transaction_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// A transaction ID unique between sending and receiving homeservers. + #[ruma_api(path)] + pub transaction_id: String, + + /// The server_name of the homeserver sending this transaction. + pub origin: String, + + /// POSIX timestamp in milliseconds on the originating homeserver when this transaction started. + #[serde(with = "ruma_serde::time::ms_since_unix_epoch")] + pub origin_server_ts: SystemTime, + + /// List of persistent updates to rooms. + /// + /// Must not be more than 50 items. + pub pdus: Vec, + + /// List of ephemeral messages. + /// + /// Must not be more than 100 items. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub edus: Vec, + } + + response { + /// Map of event IDs and response for each PDU given in the request. + #[serde(with = "crate::serde::pdu_process_response")] + pub pdus: BTreeMap>, + } +} + +/// Type for passing ephemeral data to homeservers. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Edu { + /// Type of the ephemeral message. + pub edu_type: String, + /// Content of ephemeral message + pub content: JsonValue, +}