diff --git a/Cargo.toml b/Cargo.toml index 99cfe4b0..adca0af2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ version = "0.0.1" [dependencies] js_int = "0.1.4" +matches = "0.1.8" ruma-api = "0.16.0-rc.3" ruma-events = "0.21.0-beta.1" ruma-identifiers = "0.16.0" diff --git a/src/lib.rs b/src/lib.rs index 4e296b85..4477bdcc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,12 +4,13 @@ use std::collections::BTreeMap; +use ::serde::{Deserialize, Serialize}; use js_int::UInt; use ruma_events::EventType; use ruma_identifiers::{EventId, RoomId, UserId}; -use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; +mod serde; pub mod unversioned; pub mod v1; pub mod v2; @@ -59,7 +60,7 @@ pub struct RoomV3Pdu { } /// Content hashes of a PDU. -#[derive(Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct EventHash { /// The SHA-256 hash. pub sha256: String, diff --git a/src/serde.rs b/src/serde.rs new file mode 100644 index 00000000..2a8eebbd --- /dev/null +++ b/src/serde.rs @@ -0,0 +1 @@ +pub mod room_state; diff --git a/src/serde/room_state.rs b/src/serde/room_state.rs new file mode 100644 index 00000000..5f3fbb65 --- /dev/null +++ b/src/serde/room_state.rs @@ -0,0 +1,150 @@ +//! A module to deserialize a RoomState struct from incorrectly specified v1 +//! send_join endpoint. +//! +//! For more information, see this [GitHub issue](https://github.com/matrix-org/matrix-doc/issues/2541). + +use std::fmt; + +use serde::{ + de::{Deserializer, Error, IgnoredAny, SeqAccess, Visitor}, + ser::{SerializeSeq, Serializer}, +}; + +use crate::v1::join::create_join_event::RoomState; + +pub fn serialize(room_state: &RoomState, serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&200)?; + seq.serialize_element(room_state)?; + seq.end() +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(RoomStateVisitor) +} + +struct RoomStateVisitor; + +impl<'de> Visitor<'de> for RoomStateVisitor { + type Value = RoomState; + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Room State response wrapped in an array.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let expected = "a two-element list in the response"; + if seq.next_element::()?.is_none() { + return Err(A::Error::invalid_length(0, &expected)); + } + + let room_state = seq + .next_element()? + .ok_or_else(|| A::Error::invalid_length(1, &expected))?; + + while let Some(IgnoredAny) = seq.next_element()? { + // ignore extra elements + } + + Ok(room_state) + } +} + +#[cfg(test)] +mod tests { + use matches::assert_matches; + use serde_json::{json, to_value as to_json_value}; + + use super::{deserialize, serialize, RoomState}; + + #[test] + fn test_deserialize_response() { + let response = json!([ + 200, + { + "origin": "example.com", + "auth_chain": [], + "state": [] + } + ]); + + let parsed = deserialize(response).unwrap(); + + assert_matches!( + parsed, + RoomState { origin, auth_chain, state } + if origin == "example.com" + && auth_chain.is_empty() + && state.is_empty() + ); + } + + #[test] + fn test_serialize_response() { + let room_state = RoomState { + origin: "matrix.org".to_string(), + auth_chain: Vec::new(), + state: Vec::new(), + }; + + let serialized = serialize(&room_state, serde_json::value::Serializer).unwrap(); + let expected = to_json_value(&json!( + [ + 200, + { + "origin": "matrix.org", + "auth_chain": [], + "state": [] + } + ] + )) + .unwrap(); + + assert_eq!(serialized, expected); + } + + #[test] + fn test_too_short_array() { + let json = json!([200]); + let failed_room_state = deserialize(json); + assert_eq!( + failed_room_state.unwrap_err().to_string(), + "invalid length 1, expected a two-element list in the response" + ); + } + + #[test] + fn test_not_an_array() { + let json = json!({ + "origin": "matrix.org", + "auth_chain": [], + "state": [] + }); + let failed_room_state = deserialize(json); + + assert_eq!( + failed_room_state.unwrap_err().to_string(), + "invalid type: map, expected Room State response wrapped in an array.", + ) + } + + #[test] + fn test_too_long_array() { + let json = json!([200, {"origin": "", "auth_chain": [], "state": []}, 200]); + assert_matches!( + deserialize(json).unwrap(), + RoomState { origin, auth_chain, state } + if origin == "" + && auth_chain.is_empty() + && state.is_empty() + ); + } +} diff --git a/src/v1.rs b/src/v1.rs index 94961924..bfcc53a7 100644 --- a/src/v1.rs +++ b/src/v1.rs @@ -2,3 +2,4 @@ pub mod get_public_rooms; pub mod get_server_version; +pub mod join; diff --git a/src/v1/join.rs b/src/v1/join.rs new file mode 100644 index 00000000..64d7df09 --- /dev/null +++ b/src/v1/join.rs @@ -0,0 +1,4 @@ +//! Endpoints for joining rooms. + +pub mod create_join_event; +pub mod create_join_event_template; diff --git a/src/v1/join/create_join_event.rs b/src/v1/join/create_join_event.rs new file mode 100644 index 00000000..464ce4ca --- /dev/null +++ b/src/v1/join/create_join_event.rs @@ -0,0 +1,115 @@ +//! [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; + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{EventJson, EventType}; +use ruma_identifiers::{EventId, RoomId, UserId}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +use crate::{EventHash, RoomV3Pdu}; + +ruma_api! { + metadata { + description: "Send a join event to a resident server.", + name: "create_join_event", + method: PUT, + path: "/_matrix/federation/v1/send_join/:room_id/:event_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room ID that is about to be joined. + #[ruma_api(path)] + pub room_id: RoomId, + /// The user ID the join event will be for. + #[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. + pub origin_server_ts: UInt, + + // 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 = "serde_json::Map::is_empty")] + pub unsigned: serde_json::Map, + /// Content hashes of the PDU. + pub hashes: EventHash, + /// Signatures for the PDU. + pub signatures: BTreeMap>, + } + + response { + /// Full state of the room. + #[ruma_api(body)] + #[serde(with = "crate::serde::room_state")] + 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, + }, + ) + } +} + +/// Full state of the room. +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct RoomState { + /// The resident server's DNS name. + 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>, + /// The room state. + pub state: Vec>, +} diff --git a/src/v1/join/create_join_event_template.rs b/src/v1/join/create_join_event_template.rs new file mode 100644 index 00000000..a25af1fd --- /dev/null +++ b/src/v1/join/create_join_event_template.rs @@ -0,0 +1,39 @@ +//! [GET /_matrix/federation/v1/make_join/{roomId}/{userId}](https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-make-join-roomid-userid) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::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.", + name: "create_join_event_template", + method: GET, + path: "/_matrix/federation/v1/make_join/:room_id/:user_id", + rate_limited: false, + requires_authentication: true, + } + + request { + /// The room ID that is about to be joined. + #[ruma_api(path)] + pub room_id: RoomId, + /// The user ID the join event will be for. + #[ruma_api(path)] + pub user_id: UserId, + #[ruma_api(query)] + /// The room versions the sending server has support for. Defaults to [1]. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub ver: Vec, + } + + response { + /// The version of the room where the server is trying to join. + pub room_version: Option, + /// An unsigned template event. + pub event: EventJson, + } +}