client-api: Implement space summary API

According to MSC2946
This commit is contained in:
Kévin Commaille 2022-02-25 19:42:58 +01:00
parent ede7601aa9
commit 84e1c919c9
No known key found for this signature in database
GPG Key ID: DD507DAE96E8245C
9 changed files with 324 additions and 8 deletions

View File

@ -4,6 +4,10 @@ Breaking changes:
* Use `Raw` for `config::set_*_account_data::Request::data`.
Improvements:
* Add support for the space summary API in `space::get_hierarchy` according to MSC2946.
# 0.13.0
Bug fixes:

View File

@ -35,6 +35,7 @@ pub mod room;
pub mod search;
pub mod server;
pub mod session;
pub mod space;
pub mod state;
pub mod sync;
pub mod tag;

View File

@ -0,0 +1,128 @@
//! Endpoints for spaces.
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;
/// A chunk of a space hierarchy response, describing one room.
///
/// To create an instance of this type, first create a `SpaceHierarchyRoomsChunkInit` and convert it
/// via `SpaceHierarchyRoomsChunk::from` / `.into()`.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct SpaceHierarchyRoomsChunk {
/// 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<Box<RoomAliasId>>,
/// The name of the room, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<Box<RoomName>>,
/// The number of members joined to the room.
pub num_joined_members: UInt,
/// The ID of the room.
pub room_id: Box<RoomId>,
/// The topic of the room, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
/// 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<Box<MxcUri>>,
/// 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<RoomType>,
/// 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<Raw<HierarchySpaceChildStateEvent>>,
}
/// Initial set of mandatory fields of `SpaceHierarchyRoomsChunk`.
///
/// This struct will not be updated even if additional fields are added to
/// `SpaceHierarchyRoomsChunk` in a new (non-breaking) release of the Matrix specification.
#[derive(Debug)]
#[allow(clippy::exhaustive_structs)]
pub struct SpaceHierarchyRoomsChunkInit {
/// The number of members joined to the room.
pub num_joined_members: UInt,
/// The ID of the room.
pub room_id: Box<RoomId>,
/// 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<Raw<HierarchySpaceChildStateEvent>>,
}
impl From<SpaceHierarchyRoomsChunkInit> for SpaceHierarchyRoomsChunk {
fn from(init: SpaceHierarchyRoomsChunkInit) -> Self {
let SpaceHierarchyRoomsChunkInit {
num_joined_members,
room_id,
world_readable,
guest_can_join,
join_rule,
children_state,
} = 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,
}
}
}

View File

@ -0,0 +1,85 @@
//! `GET /_matrix/client/*/rooms/{roomId}/hierarchy`
pub mod v1 {
//! `/v1/` ([spec])
//!
//! [spec]: https://spec.matrix.org/v1.2/client-server-api/#get_matrixclientv1roomsroomidhierarchy
use js_int::UInt;
use ruma_api::ruma_api;
use ruma_identifiers::RoomId;
use crate::space::SpaceHierarchyRoomsChunk;
ruma_api! {
metadata: {
description: "Paginates over the space tree in a depth-first manner to locate child rooms of a given space.",
method: GET,
name: "hierarchy",
unstable_path: "/_matrix/client/unstable/org.matrix.msc2946/rooms/:room_id/hierarchy",
stable_path: "/_matrix/client/v1/rooms/:room_id/hierarchy",
rate_limited: true,
authentication: AccessToken,
added: 1.2,
}
request: {
/// The room ID of the space to get a hierarchy for.
#[ruma_api(path)]
pub room_id: &'a RoomId,
/// A pagination token from a previous result.
///
/// If specified, `max_depth` and `suggested_only` cannot be changed from the first request.
#[ruma_api(query)]
pub from: Option<&'a str>,
/// The maximum number of rooms to include per response.
#[ruma_api(query)]
pub limit: Option<UInt>,
/// How far to go into the space.
///
/// When reached, no further child rooms will be returned.
#[ruma_api(query)]
pub max_depth: Option<UInt>,
/// Whether or not the server should only consider suggested rooms.
///
/// Suggested rooms are annotated in their `m.space.child` event contents.
///
/// Defaults to `false`.
#[ruma_api(query)]
#[serde(default, skip_serializing_if = "ruma_serde::is_default")]
pub suggested_only: bool,
}
#[derive(Default)]
response: {
/// A token to supply to from to keep paginating the responses.
///
/// Not present when there are no further results.
#[serde(skip_serializing_if = "Option::is_none")]
pub next_batch: Option<String>,
/// A paginated chunk of the space children.
pub chunk: Vec<SpaceHierarchyRoomsChunk>,
}
error: crate::Error
}
impl<'a> Request<'a> {
/// Creates a new `Request` with the given room ID.
pub fn new(room_id: &'a RoomId) -> Self {
Self { room_id, from: None, limit: None, max_depth: None, suggested_only: false }
}
}
impl Response {
/// Creates an empty `Response`.
pub fn new() -> Self {
Default::default()
}
}
}

View File

@ -357,6 +357,9 @@ pub enum EventKind {
/// Presence event kind.
Presence,
/// Hierarchy space child kind.
HierarchySpaceChild,
}
/// `HasDeserializeFields` is used in the code generated by the `Event` derive

View File

@ -2,8 +2,9 @@
//!
//! [`m.space.child`]: https://spec.matrix.org/v1.2/client-server-api/#mspacechild
use ruma_identifiers::ServerName;
use ruma_macros::EventContent;
use ruma_common::MilliSecondsSinceUnixEpoch;
use ruma_identifiers::{ServerName, UserId};
use ruma_macros::{Event, EventContent};
use serde::{Deserialize, Serialize};
/// The content of an `m.space.child` event.
@ -50,12 +51,33 @@ impl SpaceChildEventContent {
}
}
/// An `m.space.child` event represented as a Stripped State Event with an added `origin_server_ts`
/// key.
#[derive(Clone, Debug, Event)]
#[cfg_attr(not(feature = "unstable-exhaustive-types"), non_exhaustive)]
pub struct HierarchySpaceChildStateEvent {
/// The content of the space child event.
pub content: SpaceChildEventContent,
/// The fully-qualified ID of the user who sent this event.
pub sender: Box<UserId>,
/// The room ID of the child.
pub state_key: String,
/// Timestamp in milliseconds on originating homeserver when this event was sent.
pub origin_server_ts: MilliSecondsSinceUnixEpoch,
}
#[cfg(test)]
mod tests {
use ruma_identifiers::server_name;
use serde_json::{json, to_value as to_json_value};
use js_int::uint;
use matches::assert_matches;
use ruma_common::MilliSecondsSinceUnixEpoch;
use ruma_identifiers::{server_name, user_id};
use serde_json::{from_value as from_json_value, json, to_value as to_json_value};
use super::SpaceChildEventContent;
use super::{HierarchySpaceChildStateEvent, SpaceChildEventContent};
#[test]
fn space_child_serialization() {
@ -82,4 +104,63 @@ mod tests {
assert_eq!(to_json_value(&content).unwrap(), json);
}
#[test]
fn hierarchy_space_child_serialization() {
let event = HierarchySpaceChildStateEvent {
content: SpaceChildEventContent {
via: Some(vec![server_name!("example.com").to_owned()]),
order: Some("uwu".to_owned()),
suggested: None,
},
sender: user_id!("@example:localhost").to_owned(),
state_key: "!child:localhost".to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch(uint!(1_629_413_349)),
};
let json = json!({
"content": {
"via": ["example.com"],
"order": "uwu",
},
"sender": "@example:localhost",
"state_key": "!child:localhost",
"origin_server_ts": 1_629_413_349,
"type": "m.space.child",
});
assert_eq!(to_json_value(&event).unwrap(), json);
}
#[test]
fn hierarchy_space_child_deserialization() {
let json = json!({
"content": {
"via": [
"example.org"
]
},
"origin_server_ts": 1_629_413_349,
"sender": "@alice:example.org",
"state_key": "!a:example.org",
"type": "m.space.child"
});
assert_matches!(
from_json_value::<HierarchySpaceChildStateEvent>(json).unwrap(),
HierarchySpaceChildStateEvent {
content: SpaceChildEventContent {
via: Some(via),
order: None,
suggested: None,
},
origin_server_ts,
sender,
state_key,
} if via[0] == "example.org"
&& origin_server_ts.get() == uint!(1_629_413_349)
&& sender == "@alice:example.org"
&& state_key == "!a:example.org"
);
}
}

View File

@ -394,7 +394,10 @@ fn generate_marker_trait_impl(
EventKind::MessageLike => quote! { MessageLikeEventContent },
EventKind::State => quote! { StateEventContent },
EventKind::ToDevice => quote! { ToDeviceEventContent },
EventKind::RoomRedaction | EventKind::Presence | EventKind::Decrypted => {
EventKind::RoomRedaction
| EventKind::Presence
| EventKind::Decrypted
| EventKind::HierarchySpaceChild => {
return Err(syn::Error::new_spanned(
ident,
"valid event kinds are GlobalAccountData, RoomAccountData, \
@ -454,7 +457,10 @@ fn generate_static_event_content_impl(
EventKind::MessageLike => quote! { MessageLike { redacted: #redacted } },
EventKind::State => quote! { State { redacted: #redacted } },
EventKind::ToDevice => quote! { ToDevice },
EventKind::RoomRedaction | EventKind::Presence | EventKind::Decrypted => {
EventKind::RoomRedaction
| EventKind::Presence
| EventKind::Decrypted
| EventKind::HierarchySpaceChild => {
unreachable!("not a valid event content kind")
}
};

View File

@ -85,6 +85,7 @@ pub enum EventKind {
ToDevice,
RoomRedaction,
Presence,
HierarchySpaceChild,
Decrypted,
}
@ -99,6 +100,7 @@ impl fmt::Display for EventKind {
EventKind::ToDevice => write!(f, "ToDeviceEvent"),
EventKind::RoomRedaction => write!(f, "RoomRedactionEvent"),
EventKind::Presence => write!(f, "PresenceEvent"),
EventKind::HierarchySpaceChild => write!(f, "HierarchySpaceChildStateEvent"),
EventKind::Decrypted => unreachable!(),
}
}
@ -204,6 +206,9 @@ pub fn to_kind_variation(ident: &Ident) -> Option<(EventKind, EventKindVariation
"RedactedSyncStateEvent" => Some((EventKind::State, EventKindVariation::RedactedSync)),
"ToDeviceEvent" => Some((EventKind::ToDevice, EventKindVariation::Full)),
"PresenceEvent" => Some((EventKind::Presence, EventKindVariation::Full)),
"HierarchySpaceChildStateEvent" => {
Some((EventKind::HierarchySpaceChild, EventKindVariation::Stripped))
}
"RoomRedactionEvent" => Some((EventKind::RoomRedaction, EventKindVariation::Full)),
"SyncRoomRedactionEvent" => Some((EventKind::RoomRedaction, EventKindVariation::Sync)),
"RedactedRoomRedactionEvent" => {

View File

@ -31,7 +31,10 @@ pub fn expand_event_type_enum(
room.push(&event.events);
}
EventKind::ToDevice => to_device.push(&event.events),
EventKind::RoomRedaction | EventKind::Presence | EventKind::Decrypted => {}
EventKind::RoomRedaction
| EventKind::Presence
| EventKind::Decrypted
| EventKind::HierarchySpaceChild => {}
}
}
let presence = vec![EventEnumEntry {