diff --git a/ruma-federation-api/CHANGELOG.md b/ruma-federation-api/CHANGELOG.md index 4336ac48..1bd904e3 100644 --- a/ruma-federation-api/CHANGELOG.md +++ b/ruma-federation-api/CHANGELOG.md @@ -16,6 +16,7 @@ Improvements: claim_keys::v1, query_keys::v1, }, + membership::create_invite::{v1, v2}, ``` # 0.0.3 diff --git a/ruma-federation-api/src/membership.rs b/ruma-federation-api/src/membership.rs index 6ca64dd8..6e5398b9 100644 --- a/ruma-federation-api/src/membership.rs +++ b/ruma-federation-api/src/membership.rs @@ -1,4 +1,5 @@ //! Room membership endpoints. +pub mod create_invite; pub mod create_join_event; pub mod create_join_event_template; diff --git a/ruma-federation-api/src/membership/create_invite.rs b/ruma-federation-api/src/membership/create_invite.rs new file mode 100644 index 00000000..83270e48 --- /dev/null +++ b/ruma-federation-api/src/membership/create_invite.rs @@ -0,0 +1,83 @@ +//! Endpoint for inviting a remote user to a room + +use js_int::UInt; +use ruma_events::{room::member::MemberEventContent, EventType}; +use ruma_identifiers::{ServerName, UserId}; +use serde::{Deserialize, Serialize}; +use serde_json::Value as JsonValue; + +pub mod v1; +pub mod v2; + +/// A simplified event that helps the server identify a room. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StrippedState { + /// The `content` for the event. + pub content: JsonValue, + + /// The `state_key` for the event. + pub state_key: String, + + /// The `type` for the event. + #[serde(rename = "type")] + pub kind: EventType, + + /// The `sender` for the event. + pub sender: UserId, +} + +/// The invite event sent as a response. +#[derive(Clone, Debug, Serialize, Deserialize)] +#[non_exhaustive] +pub struct InviteEvent { + /// The matrix ID of the user who sent the original `m.room.third_party_invite`. + pub sender: UserId, + + /// The name of the inviting homeserver. + pub origin: Box, + + /// A timestamp added by the inviting homeserver. + pub origin_server_ts: UInt, + + /// The event type (should always be `m.room.member`). + #[serde(rename = "type")] + pub kind: EventType, + + /// The user ID of the invited member. + pub state_key: UserId, + + /// The content of the event. Must include a `membership` of invite. + pub content: MemberEventContent, +} + +/// Initial set of fields for `Response`. +pub struct InviteEventInit { + /// The matrix ID of the user who sent the original `m.room.third_party_invite`. + pub sender: UserId, + + /// The name of the inviting homeserver. + pub origin: Box, + + /// A timestamp added by the inviting homeserver. + pub origin_server_ts: UInt, + + /// The user ID of the invited member. + pub state_key: UserId, + + /// The content of the event. Must include a `membership` of invite. + pub content: MemberEventContent, +} + +impl From for InviteEvent { + /// Creates a new `Response` with the given inital values + fn from(init: InviteEventInit) -> Self { + InviteEvent { + sender: init.sender, + origin: init.origin, + origin_server_ts: init.origin_server_ts, + kind: EventType::RoomMember, + state_key: init.state_key, + content: init.content, + } + } +} diff --git a/ruma-federation-api/src/membership/create_invite/v1.rs b/ruma-federation-api/src/membership/create_invite/v1.rs new file mode 100644 index 00000000..63c9b567 --- /dev/null +++ b/ruma-federation-api/src/membership/create_invite/v1.rs @@ -0,0 +1,121 @@ +//! [PUT /_matrix/federation/v1/invite/{roomId}/{eventId}](https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v1-invite-roomid-eventid) + +use js_int::UInt; +use ruma_api::ruma_api; +use ruma_events::{room::member::MemberEventContent, EventType}; +use ruma_identifiers::{EventId, RoomId, ServerName, UserId}; +use serde::{Deserialize, Serialize}; + +use super::{InviteEvent, StrippedState}; + +ruma_api! { + metadata: { + description: "Invites a remote user to a room.", + method: PUT, + name: "create_invite", + path: "/_matrix/federation/v1/invite/:room_id/:event_id", + rate_limited: false, + requires_authentication: true, + } + + #[non_exhaustive] + request: { + /// The room ID that the user is being invited to. + #[ruma_api(path)] + pub room_id: RoomId, + + /// The event ID for the invite event, generated by the inviting server. + #[ruma_api(path)] + pub event_id: EventId, + + /// The matrix ID of the user who sent the original `m.room.third_party_invite`. + pub sender: UserId, + + /// The name of the inviting homeserver. + pub origin: Box, + + /// A timestamp added by the inviting homeserver. + pub origin_server_ts: UInt, + + /// The value `m.room.member`. + #[serde(rename = "type")] + pub kind: EventType, + + /// The user ID of the invited member. + pub state_key: UserId, + + /// The content of the event. + pub content: MemberEventContent, + + /// Information included alongside the event that is not signed. + pub unsigned: UnsignedEventContent, + } + + #[non_exhaustive] + response: { + /// The response invite event + #[ruma_api(body)] + #[serde(with = "crate::serde::invite_response")] + event: InviteEvent, + } +} + +/// Information included alongside an event that is not signed. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct UnsignedEventContent { + /// An optional list of simplified events to help the receiver of the invite identify the room. + /// The recommended events to include are the join rules, canonical alias, avatar, and name of + /// the room. + pub invite_room_state: Vec, +} + +/// Initial set of fields of `Request`. +pub struct RequestInit { + /// The room ID that the user is being invited to. + pub room_id: RoomId, + + /// The event ID for the invite event, generated by the inviting server. + pub event_id: EventId, + + /// The matrix ID of the user who sent the original `m.room.third_party_invite`. + pub sender: UserId, + + /// The name of the inviting homeserver. + pub origin: Box, + + /// A timestamp added by the inviting homeserver. + pub origin_server_ts: UInt, + + /// The user ID of the invited member. + pub state_key: UserId, + + /// The content of the event. + pub content: MemberEventContent, + + /// Information included alongside the event that is not signed. + pub unsigned: UnsignedEventContent, +} + +impl From for Request { + /// Creates a new `Request` with the given parameters. + fn from(init: RequestInit) -> Self { + Self { + room_id: init.room_id, + event_id: init.event_id, + sender: init.sender, + origin: init.origin, + origin_server_ts: init.origin_server_ts, + kind: EventType::RoomMember, + state_key: init.state_key, + content: init.content, + unsigned: init.unsigned, + } + } +} + +impl Response { + /// Creates a new `Response` with the given invite event. + pub fn new(event: InviteEvent) -> Self { + Self { event } + } +} diff --git a/ruma-federation-api/src/membership/create_invite/v2.rs b/ruma-federation-api/src/membership/create_invite/v2.rs new file mode 100644 index 00000000..87ec05aa --- /dev/null +++ b/ruma-federation-api/src/membership/create_invite/v2.rs @@ -0,0 +1,63 @@ +//! [PUT /_matrix/federation/v2/invite/{roomId}/{eventId}](https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v2-invite-roomid-eventid) + +use ruma_api::ruma_api; +use ruma_identifiers::{EventId, RoomId, RoomVersionId}; + +use super::{InviteEvent, StrippedState}; + +ruma_api! { + metadata: { + description: "Invites a remote user to a room.", + method: PUT, + name: "create_invite", + path: "/_matrix/federation/v2/invite/:room_id/:event_id", + rate_limited: false, + requires_authentication: true, + } + + #[non_exhaustive] + request: { + /// The room ID that the user is being invited to. + #[ruma_api(path)] + room_id: RoomId, + + /// The event ID for the invite event, generated by the inviting server. + #[ruma_api(path)] + event_id: EventId, + + /// The version of the room where the user is being invited to. + room_version: RoomVersionId, + + /// An invite event. + event: InviteEvent, + + /// An optional list of simplified events to help the receiver of the invite identify the room. + invite_room_state: StrippedState, + } + + #[non_exhaustive] + response: { + /// An invite event. + event: InviteEvent, + } +} + +impl Request { + /// Creates a new `Request` with the given parameters + pub fn new( + room_id: RoomId, + event_id: EventId, + room_version: RoomVersionId, + event: InviteEvent, + invite_room_state: StrippedState, + ) -> Self { + Self { room_id, event_id, room_version, event, invite_room_state } + } +} + +impl Response { + /// Creates a new `Response` with the given invite event. + pub fn new(event: InviteEvent) -> Self { + Self { event } + } +} diff --git a/ruma-federation-api/src/serde.rs b/ruma-federation-api/src/serde.rs index 9054a487..bdf383a4 100644 --- a/ruma-federation-api/src/serde.rs +++ b/ruma-federation-api/src/serde.rs @@ -1,4 +1,5 @@ //! Modules for custom serde de/-serialization implementations. +pub mod invite_response; pub mod pdu_process_response; pub mod room_state; diff --git a/ruma-federation-api/src/serde/invite_response.rs b/ruma-federation-api/src/serde/invite_response.rs new file mode 100644 index 00000000..b0ed8057 --- /dev/null +++ b/ruma-federation-api/src/serde/invite_response.rs @@ -0,0 +1,60 @@ +//! Deserialization for `InviteEvent` from incorrectly specified `create_invite` endpoint. +//! +//! See [this GitHub issue][issue] for more information. +//! +//! [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::membership::create_invite::InviteEvent; + +pub fn serialize(invite_response: &InviteEvent, serializer: S) -> Result +where + S: Serializer, +{ + let mut seq = serializer.serialize_seq(Some(2))?; + seq.serialize_element(&200)?; + seq.serialize_element(invite_response)?; + seq.end() +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + deserializer.deserialize_seq(InviteEventVisitor) +} + +struct InviteEventVisitor; + +impl<'de> Visitor<'de> for InviteEventVisitor { + type Value = InviteEvent; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("Invite 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"; + // Ignore first list element (200 http status code). + if seq.next_element::()?.is_none() { + return Err(A::Error::invalid_length(0, &expected)); + } + + let invite_event = + seq.next_element()?.ok_or_else(|| A::Error::invalid_length(1, &expected))?; + + // Ignore extra elements. + while let Some(IgnoredAny) = seq.next_element()? {} + + Ok(invite_event) + } +}