diff --git a/crates/ruma-federation-api/CHANGELOG.md b/crates/ruma-federation-api/CHANGELOG.md index 5b8a66c6..3b2d79dd 100644 --- a/crates/ruma-federation-api/CHANGELOG.md +++ b/crates/ruma-federation-api/CHANGELOG.md @@ -1,5 +1,9 @@ # [unreleased] +Improvements: + +* Add support for the space summary API in `space::get_hierarchy` according to MSC2946. + # 0.4.0 Breaking changes: diff --git a/crates/ruma-federation-api/src/lib.rs b/crates/ruma-federation-api/src/lib.rs index 911afc4d..bb917c0e 100644 --- a/crates/ruma-federation-api/src/lib.rs +++ b/crates/ruma-federation-api/src/lib.rs @@ -23,6 +23,7 @@ pub mod knock; pub mod membership; pub mod openid; pub mod query; +pub mod space; pub mod thirdparty; pub mod transactions; diff --git a/crates/ruma-federation-api/src/space.rs b/crates/ruma-federation-api/src/space.rs new file mode 100644 index 00000000..dda4c2d9 --- /dev/null +++ b/crates/ruma-federation-api/src/space.rs @@ -0,0 +1,256 @@ +//! Spaces endpoints. + +use js_int::UInt; +use ruma_common::{directory::PublicRoomJoinRule, room::RoomType}; +use ruma_events::space::child::HierarchySpaceChildStateEvent; +use ruma_identifiers::{MxcUri, RoomAliasId, RoomId, RoomName}; +use ruma_serde::Raw; +use serde::{Deserialize, Serialize}; + +pub mod get_hierarchy; + +/// The summary of a parent space. +/// +/// To create an instance of this type, first create a `SpaceHierarchyParentSummaryInit` and convert +/// it via `SpaceHierarchyParentSummary::from` / `.into()`. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SpaceHierarchyParentSummary { + /// The canonical alias of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "compat", + serde(default, deserialize_with = "ruma_serde::empty_string_as_none") + )] + pub canonical_alias: Option>, + + /// The name of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option>, + + /// The number of members joined to the room. + pub num_joined_members: UInt, + + /// The ID of the room. + pub room_id: Box, + + /// The topic of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + /// Whether the room may be viewed by guest users without joining. + pub world_readable: bool, + + /// Whether guest users may join the room and participate in it. + /// + /// If they can, they will be subject to ordinary power level rules like any other user. + pub guest_can_join: bool, + + /// The URL for the room's avatar, if one is set. + /// + /// If you activate the `compat` feature, this field being an empty string in JSON will result + /// in `None` here during deserialization. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "compat", + serde(default, deserialize_with = "ruma_serde::empty_string_as_none") + )] + pub avatar_url: Option>, + + /// The join rule of the room. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub join_rule: PublicRoomJoinRule, + + /// The type of room from `m.room.create`, if any. + pub room_type: Option, + + /// The stripped `m.space.child` events of the space-room. + /// + /// If the room is not a space-room, this should be empty. + pub children_state: Vec>, + + /// If the room is a restricted room, these are the room IDs which are specified by the join + /// rules. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub allowed_room_ids: Vec>, +} + +/// Initial set of mandatory fields of `SpaceHierarchyParentSummary`. +/// +/// This struct will not be updated even if additional fields are added to +/// `SpaceHierarchyParentSummary` in a new (non-breaking) release of the Matrix specification. +#[derive(Debug)] +#[allow(clippy::exhaustive_structs)] +pub struct SpaceHierarchyParentSummaryInit { + /// The number of members joined to the room. + pub num_joined_members: UInt, + + /// The ID of the room. + pub room_id: Box, + + /// Whether the room may be viewed by guest users without joining. + pub world_readable: bool, + + /// Whether guest users may join the room and participate in it. + /// + /// If they can, they will be subject to ordinary power level rules like any other user. + pub guest_can_join: bool, + + /// The join rule of the room. + pub join_rule: PublicRoomJoinRule, + + /// The stripped `m.space.child` events of the space-room. + /// + /// If the room is not a space-room, this should be empty. + pub children_state: Vec>, + + /// If the room is a restricted room, these are the room IDs which are specified by the join + /// rules. + pub allowed_room_ids: Vec>, +} + +impl From for SpaceHierarchyParentSummary { + fn from(init: SpaceHierarchyParentSummaryInit) -> Self { + let SpaceHierarchyParentSummaryInit { + num_joined_members, + room_id, + world_readable, + guest_can_join, + join_rule, + children_state, + allowed_room_ids, + } = init; + + Self { + canonical_alias: None, + name: None, + num_joined_members, + room_id, + topic: None, + world_readable, + guest_can_join, + avatar_url: None, + join_rule, + room_type: None, + children_state, + allowed_room_ids, + } + } +} + +/// The summary of a space's child. +/// +/// To create an instance of this type, first create a `SpaceHierarchyChildSummaryInit` and convert +/// it via `SpaceHierarchyChildSummary::from` / `.into()`. +#[derive(Clone, Debug, Deserialize, Serialize)] +#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)] +pub struct SpaceHierarchyChildSummary { + /// The canonical alias of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "compat", + serde(default, deserialize_with = "ruma_serde::empty_string_as_none") + )] + pub canonical_alias: Option>, + + /// The name of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option>, + + /// The number of members joined to the room. + pub num_joined_members: UInt, + + /// The ID of the room. + pub room_id: Box, + + /// The topic of the room, if any. + #[serde(skip_serializing_if = "Option::is_none")] + pub topic: Option, + + /// Whether the room may be viewed by guest users without joining. + pub world_readable: bool, + + /// Whether guest users may join the room and participate in it. + /// + /// If they can, they will be subject to ordinary power level rules like any other user. + pub guest_can_join: bool, + + /// The URL for the room's avatar, if one is set. + /// + /// If you activate the `compat` feature, this field being an empty string in JSON will result + /// in `None` here during deserialization. + #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr( + feature = "compat", + serde(default, deserialize_with = "ruma_serde::empty_string_as_none") + )] + pub avatar_url: Option>, + + /// The join rule of the room. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub join_rule: PublicRoomJoinRule, + + /// The type of room from `m.room.create`, if any. + pub room_type: Option, + + /// If the room is a restricted room, these are the room IDs which are specified by the join + /// rules. + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub allowed_room_ids: Vec>, +} + +/// Initial set of mandatory fields of `SpaceHierarchyChildSummary`. +/// +/// This struct will not be updated even if additional fields are added to +/// `SpaceHierarchyChildSummary` in a new (non-breaking) release of the Matrix specification. +#[derive(Debug)] +#[allow(clippy::exhaustive_structs)] +pub struct SpaceHierarchyChildSummaryInit { + /// The number of members joined to the room. + pub num_joined_members: UInt, + + /// The ID of the room. + pub room_id: Box, + + /// Whether the room may be viewed by guest users without joining. + pub world_readable: bool, + + /// Whether guest users may join the room and participate in it. + /// + /// If they can, they will be subject to ordinary power level rules like any other user. + pub guest_can_join: bool, + + /// The join rule of the room. + pub join_rule: PublicRoomJoinRule, + + /// If the room is a restricted room, these are the room IDs which are specified by the join + /// rules. + pub allowed_room_ids: Vec>, +} + +impl From for SpaceHierarchyChildSummary { + fn from(init: SpaceHierarchyChildSummaryInit) -> Self { + let SpaceHierarchyChildSummaryInit { + num_joined_members, + room_id, + world_readable, + guest_can_join, + join_rule, + allowed_room_ids, + } = init; + + Self { + canonical_alias: None, + name: None, + num_joined_members, + room_id, + topic: None, + world_readable, + guest_can_join, + avatar_url: None, + join_rule, + room_type: None, + allowed_room_ids, + } + } +} diff --git a/crates/ruma-federation-api/src/space/get_hierarchy.rs b/crates/ruma-federation-api/src/space/get_hierarchy.rs new file mode 100644 index 00000000..29a7cb1f --- /dev/null +++ b/crates/ruma-federation-api/src/space/get_hierarchy.rs @@ -0,0 +1,70 @@ +//! `GET /_matrix/federation/*/hierarchy/{roomId}` +//! +//! Endpoint to get the children of a given space. + +pub mod v1 { + //! `/v1/` ([spec]) + //! + //! [spec]: https://spec.matrix.org/v1.2/server-server-api/#get_matrixfederationv1hierarchyroomid + + use ruma_api::ruma_api; + use ruma_identifiers::RoomId; + + use crate::space::{SpaceHierarchyChildSummary, SpaceHierarchyParentSummary}; + + ruma_api! { + metadata: { + description: "Get the space tree in a depth-first manner to locate child rooms of a given space.", + name: "hierarchy", + method: GET, + unstable_path: "/_matrix/federation/unstable/org.matrix.msc2946/hierarchy/:room_id", + stable_path: "/_matrix/federation/v1/hierarchy/:room_id", + rate_limited: false, + authentication: ServerSignatures, + added: 1.2, + } + + request: { + /// The room ID of the space to get a hierarchy for. + #[ruma_api(path)] + pub room_id: &'a RoomId, + + /// Whether or not the server should only consider suggested rooms. + /// + /// Suggested rooms are annotated in their `m.space.child` event contents. + #[ruma_api(query)] + #[serde(default, skip_serializing_if = "ruma_serde::is_default")] + pub suggested_only: bool, + } + + response: { + /// A summary of the space’s children. + /// + /// Rooms which the requesting server cannot peek/join will be excluded. + pub children: Vec, + + /// The list of room IDs the requesting server doesn’t have a viable way to peek/join. + /// + /// Rooms which the responding server cannot provide details on will be outright + /// excluded from the response instead. + pub inaccessible_children: Vec>, + + /// A summary of the requested room. + pub room: SpaceHierarchyParentSummary, + } + } + + impl<'a> Request<'a> { + /// Creates a `Request` with the given room ID. + pub fn new(room_id: &'a RoomId) -> Self { + Self { room_id, suggested_only: false } + } + } + + impl Response { + /// Creates a new `Response` with the given room summary. + pub fn new(room_summary: SpaceHierarchyParentSummary) -> Self { + Self { children: Vec::new(), inaccessible_children: Vec::new(), room: room_summary } + } + } +}