client-api: Implement space summary API
According to MSC2946
This commit is contained in:
parent
ede7601aa9
commit
84e1c919c9
@ -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:
|
||||
|
@ -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;
|
||||
|
128
crates/ruma-client-api/src/space.rs
Normal file
128
crates/ruma-client-api/src/space.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
85
crates/ruma-client-api/src/space/get_hierarchy.rs
Normal file
85
crates/ruma-client-api/src/space/get_hierarchy.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
||||
};
|
||||
|
@ -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" => {
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user