Add client space api '/rooms/{roomId}/hierarchy'

This commit is contained in:
chenyuqide 2022-04-10 18:57:15 +08:00 committed by Nyaaori
parent d2cc9e105a
commit 03029711fe
No known key found for this signature in database
GPG key ID: E7819C3ED4D1F82E
3 changed files with 245 additions and 0 deletions

View file

@ -20,6 +20,7 @@ mod report;
mod room;
mod search;
mod session;
mod space;
mod state;
mod sync;
mod tag;
@ -52,6 +53,7 @@ pub use report::*;
pub use room::*;
pub use search::*;
pub use session::*;
pub use space::*;
pub use state::*;
pub use sync::*;
pub use tag::*;

View file

@ -0,0 +1,242 @@
use std::{collections::HashSet, sync::Arc};
use crate::{services, Error, PduEvent, Result, Ruma};
use ruma::{
api::client::{
error::ErrorKind,
space::{get_hierarchy, SpaceHierarchyRoomsChunk, SpaceRoomJoinRule},
},
events::{
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
name::RoomNameEventContent,
topic::RoomTopicEventContent,
},
space::child::{HierarchySpaceChildEvent, SpaceChildEventContent},
StateEventType,
},
serde::Raw,
MilliSecondsSinceUnixEpoch, OwnedRoomId, RoomId,
};
use serde_json;
use tracing::warn;
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy``
///
/// Paginates over the space tree in a depth-first manner to locate child rooms of a given space.
///
/// - TODO: Use federation for unknown room.
///
pub async fn get_hierarchy_route(
body: Ruma<get_hierarchy::v1::IncomingRequest>,
) -> Result<get_hierarchy::v1::Response> {
// from format is '{suggested_only}|{max_depth}|{skip}'
let (suggested_only, max_depth, start) = body
.from
.as_ref()
.map_or(
Some((
body.suggested_only,
body.max_depth
.map_or(services().globals.hierarchy_max_depth(), |v| v.into())
.min(services().globals.hierarchy_max_depth()),
0,
)),
|from| {
let mut p = from.split('|');
Some((
p.next()?.trim().parse().ok()?,
p.next()?
.trim()
.parse::<u64>()
.ok()?
.min(services().globals.hierarchy_max_depth()),
p.next()?.trim().parse().ok()?,
))
},
)
.ok_or(Error::BadRequest(ErrorKind::InvalidParam, "Invalid from"))?;
let limit = body.limit.map_or(20u64, |v| v.into()) as usize;
let mut skip = start;
// Set for avoid search in loop.
let mut room_set = HashSet::new();
let mut rooms_chunk: Vec<SpaceHierarchyRoomsChunk> = vec![];
let mut stack = vec![(0, body.room_id.clone())];
while let (Some((depth, room_id)), true) = (stack.pop(), rooms_chunk.len() < limit) {
let (childern, pdus): (Vec<_>, Vec<_>) = services()
.rooms
.state_accessor
.room_state_full(&room_id)
.await?
.into_iter()
.filter_map(|((e_type, key), pdu)| {
(e_type == StateEventType::SpaceChild && !room_set.contains(&room_id))
.then_some((key, pdu))
})
.unzip();
if skip == 0 {
if rooms_chunk.len() < limit {
room_set.insert(room_id.clone());
rooms_chunk.push(get_room_chunk(room_id, suggested_only, pdus).await?);
}
} else {
skip -= 1;
}
if depth < max_depth {
childern.into_iter().rev().for_each(|key| {
stack.push((depth + 1, RoomId::parse(key).unwrap()));
});
}
}
Ok(get_hierarchy::v1::Response {
next_batch: (!stack.is_empty()).then_some(format!(
"{}|{}|{}",
suggested_only,
max_depth,
start + limit
)),
rooms: rooms_chunk,
})
}
async fn get_room_chunk(
room_id: OwnedRoomId,
suggested_only: bool,
phus: Vec<Arc<PduEvent>>,
) -> Result<SpaceHierarchyRoomsChunk> {
Ok(SpaceHierarchyRoomsChunk {
canonical_alias: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")
.ok()
.and_then(|s| {
serde_json::from_str(s?.content.get())
.map(|c: RoomCanonicalAliasEventContent| c.alias)
.ok()?
}),
name: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomName, "")
.ok()
.flatten()
.and_then(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomNameEventContent| c.name)
.ok()?
}),
num_joined_members: services()
.rooms
.state_cache
.room_joined_count(&room_id)?
.unwrap_or_else(|| {
warn!("Room {} has no member count", &room_id);
0
})
.try_into()
.expect("user count should not be that big"),
topic: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomTopic, "")
.ok()
.and_then(|s| {
serde_json::from_str(s?.content.get())
.ok()
.map(|c: RoomTopicEventContent| c.topic)
}),
world_readable: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
})
.map_err(|_| {
Error::bad_database("Invalid room history visibility event in database.")
})
})?,
guest_can_join: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomGuestAccessEventContent| c.guest_access == GuestAccess::CanJoin)
.map_err(|_| {
Error::bad_database("Invalid room guest access event in database.")
})
})?,
avatar_url: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")
.ok()
.and_then(|s| {
serde_json::from_str(s?.content.get())
.map(|c: RoomAvatarEventContent| c.url)
.ok()?
}),
join_rule: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomJoinRulesEventContent| match c.join_rule {
JoinRule::Invite => SpaceRoomJoinRule::Invite,
JoinRule::Knock => SpaceRoomJoinRule::Knock,
JoinRule::Private => SpaceRoomJoinRule::Private,
JoinRule::Public => SpaceRoomJoinRule::Public,
JoinRule::Restricted(_) => SpaceRoomJoinRule::Restricted,
// Can't convert two type.
JoinRule::_Custom(_) => SpaceRoomJoinRule::Private,
})
.map_err(|_| Error::bad_database("Invalid room join rules event in database."))
})
.ok_or_else(|| Error::bad_database("Invalid room join rules event in database."))??,
room_type: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")
.map(|s| {
serde_json::from_str(s?.content.get())
.map(|c: RoomCreateEventContent| c.room_type)
.ok()?
})
.ok()
.flatten(),
children_state: phus
.into_iter()
.flat_map(|pdu| {
Some(HierarchySpaceChildEvent {
// Ignore unsuggested rooms if suggested_only is set
content: serde_json::from_str(pdu.content.get()).ok().filter(
|pdu: &SpaceChildEventContent| {
!suggested_only || pdu.suggested.unwrap_or(false)
},
)?,
sender: pdu.sender.clone(),
state_key: pdu.state_key.clone()?,
origin_server_ts: MilliSecondsSinceUnixEpoch(pdu.origin_server_ts),
})
})
.filter_map(|hsce| Raw::<HierarchySpaceChildEvent>::new(&hsce).ok())
.collect::<Vec<_>>(),
room_id,
})
}

View file

@ -318,6 +318,7 @@ fn routes() -> Router {
.ruma_route(client_server::send_state_event_for_key_route)
.ruma_route(client_server::get_state_events_route)
.ruma_route(client_server::get_state_events_for_key_route)
.ruma_route(client_server::get_hierarchy_route)
// Ruma doesn't have support for multiple paths for a single endpoint yet, and these routes
// share one Ruma request / response type pair with {get,send}_state_event_for_key_route
.route(