2025-02-06 20:08:00 +00:00
|
|
|
use std::{collections::VecDeque, str::FromStr};
|
2023-07-02 16:06:54 +02:00
|
|
|
|
2024-07-16 08:05:25 +00:00
|
|
|
use axum::extract::State;
|
2025-02-06 20:08:00 +00:00
|
|
|
use conduwuit::{checked, pdu::ShortRoomId, utils::stream::IterStream};
|
|
|
|
use futures::{StreamExt, TryFutureExt};
|
2024-03-16 16:09:11 -04:00
|
|
|
use ruma::{
|
|
|
|
api::client::{error::ErrorKind, space::get_hierarchy},
|
2025-02-06 20:08:00 +00:00
|
|
|
OwnedRoomId, OwnedServerName, RoomId, UInt, UserId,
|
|
|
|
};
|
|
|
|
use service::{
|
|
|
|
rooms::spaces::{get_parent_children_via, summary_to_chunk, SummaryAccessibility},
|
|
|
|
Services,
|
2024-03-16 16:09:11 -04:00
|
|
|
};
|
|
|
|
|
2024-07-16 08:05:25 +00:00
|
|
|
use crate::{service::rooms::spaces::PaginationToken, Error, Result, Ruma};
|
2024-03-05 19:48:54 -05:00
|
|
|
|
2024-03-22 21:51:21 -04:00
|
|
|
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`
|
2023-07-02 16:06:54 +02:00
|
|
|
///
|
|
|
|
/// Paginates over the space tree in a depth-first manner to locate child rooms
|
|
|
|
/// of a given space.
|
2024-07-16 08:05:25 +00:00
|
|
|
pub(crate) async fn get_hierarchy_route(
|
2024-12-15 00:05:47 -05:00
|
|
|
State(services): State<crate::State>,
|
|
|
|
body: Ruma<get_hierarchy::v1::Request>,
|
2024-07-16 08:05:25 +00:00
|
|
|
) -> Result<get_hierarchy::v1::Response> {
|
2024-07-02 16:42:07 -04:00
|
|
|
let limit = body
|
2024-03-25 17:05:11 -04:00
|
|
|
.limit
|
2024-07-02 16:42:07 -04:00
|
|
|
.unwrap_or_else(|| UInt::from(10_u32))
|
|
|
|
.min(UInt::from(100_u32));
|
2024-03-25 17:05:11 -04:00
|
|
|
|
|
|
|
let max_depth = body
|
|
|
|
.max_depth
|
|
|
|
.unwrap_or_else(|| UInt::from(3_u32))
|
|
|
|
.min(UInt::from(10_u32));
|
|
|
|
|
|
|
|
let key = body
|
|
|
|
.from
|
|
|
|
.as_ref()
|
2024-07-02 16:42:07 -04:00
|
|
|
.and_then(|s| PaginationToken::from_str(s).ok());
|
2023-07-02 16:06:54 +02:00
|
|
|
|
2024-03-16 16:09:11 -04:00
|
|
|
// Should prevent unexpeded behaviour in (bad) clients
|
|
|
|
if let Some(ref token) = key {
|
|
|
|
if token.suggested_only != body.suggested_only || token.max_depth != max_depth {
|
|
|
|
return Err(Error::BadRequest(
|
|
|
|
ErrorKind::InvalidParam,
|
|
|
|
"suggested_only and max_depth cannot change on paginated requests",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
2023-07-02 16:06:54 +02:00
|
|
|
|
2025-02-06 20:08:00 +00:00
|
|
|
get_client_hierarchy(
|
|
|
|
&services,
|
|
|
|
body.sender_user(),
|
|
|
|
&body.room_id,
|
|
|
|
limit.try_into().unwrap_or(10),
|
|
|
|
key.map_or(vec![], |token| token.short_room_ids),
|
|
|
|
max_depth.into(),
|
|
|
|
body.suggested_only,
|
|
|
|
)
|
|
|
|
.await
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn get_client_hierarchy(
|
|
|
|
services: &Services,
|
|
|
|
sender_user: &UserId,
|
|
|
|
room_id: &RoomId,
|
|
|
|
limit: usize,
|
|
|
|
short_room_ids: Vec<ShortRoomId>,
|
|
|
|
max_depth: u64,
|
|
|
|
suggested_only: bool,
|
|
|
|
) -> Result<get_hierarchy::v1::Response> {
|
|
|
|
let mut parents = VecDeque::new();
|
|
|
|
|
|
|
|
// Don't start populating the results if we have to start at a specific room.
|
|
|
|
let mut populate_results = short_room_ids.is_empty();
|
|
|
|
|
|
|
|
let mut stack = vec![vec![(room_id.to_owned(), match room_id.server_name() {
|
|
|
|
| Some(server_name) => vec![server_name.into()],
|
|
|
|
| None => vec![],
|
|
|
|
})]];
|
|
|
|
|
|
|
|
let mut results = Vec::with_capacity(limit);
|
|
|
|
|
|
|
|
while let Some((current_room, via)) = { next_room_to_traverse(&mut stack, &mut parents) } {
|
|
|
|
if results.len() >= limit {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
match (
|
|
|
|
services
|
|
|
|
.rooms
|
|
|
|
.spaces
|
|
|
|
.get_summary_and_children_client(¤t_room, suggested_only, sender_user, &via)
|
|
|
|
.await?,
|
|
|
|
current_room == room_id,
|
|
|
|
) {
|
|
|
|
| (Some(SummaryAccessibility::Accessible(summary)), _) => {
|
|
|
|
let mut children: Vec<(OwnedRoomId, Vec<OwnedServerName>)> =
|
|
|
|
get_parent_children_via(&summary, suggested_only)
|
|
|
|
.into_iter()
|
|
|
|
.filter(|(room, _)| parents.iter().all(|parent| parent != room))
|
|
|
|
.rev()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if populate_results {
|
|
|
|
results.push(summary_to_chunk(*summary.clone()));
|
|
|
|
} else {
|
|
|
|
children = children
|
|
|
|
.iter()
|
|
|
|
.rev()
|
|
|
|
.stream()
|
|
|
|
.skip_while(|(room, _)| {
|
|
|
|
services
|
|
|
|
.rooms
|
|
|
|
.short
|
|
|
|
.get_shortroomid(room)
|
|
|
|
.map_ok(|short| Some(&short) != short_room_ids.get(parents.len()))
|
|
|
|
.unwrap_or_else(|_| false)
|
|
|
|
})
|
|
|
|
.map(Clone::clone)
|
|
|
|
.collect::<Vec<(OwnedRoomId, Vec<OwnedServerName>)>>()
|
|
|
|
.await
|
|
|
|
.into_iter()
|
|
|
|
.rev()
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
if children.is_empty() {
|
|
|
|
return Err(Error::BadRequest(
|
|
|
|
ErrorKind::InvalidParam,
|
|
|
|
"Room IDs in token were not found.",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
// We have reached the room after where we last left off
|
|
|
|
let parents_len = parents.len();
|
|
|
|
if checked!(parents_len + 1)? == short_room_ids.len() {
|
|
|
|
populate_results = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let parents_len: u64 = parents.len().try_into()?;
|
|
|
|
if !children.is_empty() && parents_len < max_depth {
|
|
|
|
parents.push_back(current_room.clone());
|
|
|
|
stack.push(children);
|
|
|
|
}
|
|
|
|
// Root room in the space hierarchy, we return an error
|
|
|
|
// if this one fails.
|
|
|
|
},
|
|
|
|
| (Some(SummaryAccessibility::Inaccessible), true) => {
|
|
|
|
return Err(Error::BadRequest(
|
|
|
|
ErrorKind::forbidden(),
|
|
|
|
"The requested room is inaccessible",
|
|
|
|
));
|
|
|
|
},
|
|
|
|
| (None, true) => {
|
|
|
|
return Err(Error::BadRequest(
|
|
|
|
ErrorKind::forbidden(),
|
|
|
|
"The requested room was not found",
|
|
|
|
));
|
|
|
|
},
|
|
|
|
// Just ignore other unavailable rooms
|
|
|
|
| (None | Some(SummaryAccessibility::Inaccessible), false) => (),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(get_hierarchy::v1::Response {
|
|
|
|
next_batch: if let Some((room, _)) = next_room_to_traverse(&mut stack, &mut parents) {
|
|
|
|
parents.pop_front();
|
|
|
|
parents.push_back(room);
|
|
|
|
|
|
|
|
let next_short_room_ids: Vec<_> = parents
|
|
|
|
.iter()
|
|
|
|
.stream()
|
|
|
|
.filter_map(|room_id| async move {
|
|
|
|
services.rooms.short.get_shortroomid(room_id).await.ok()
|
|
|
|
})
|
|
|
|
.collect()
|
|
|
|
.await;
|
|
|
|
|
|
|
|
(next_short_room_ids != short_room_ids && !next_short_room_ids.is_empty()).then(
|
|
|
|
|| {
|
|
|
|
PaginationToken {
|
|
|
|
short_room_ids: next_short_room_ids,
|
|
|
|
limit: UInt::new(max_depth)
|
|
|
|
.expect("When sent in request it must have been valid UInt"),
|
|
|
|
max_depth: UInt::new(max_depth)
|
|
|
|
.expect("When sent in request it must have been valid UInt"),
|
|
|
|
suggested_only,
|
|
|
|
}
|
|
|
|
.to_string()
|
|
|
|
},
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
rooms: results,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn next_room_to_traverse(
|
|
|
|
stack: &mut Vec<Vec<(OwnedRoomId, Vec<OwnedServerName>)>>,
|
|
|
|
parents: &mut VecDeque<OwnedRoomId>,
|
|
|
|
) -> Option<(OwnedRoomId, Vec<OwnedServerName>)> {
|
|
|
|
while stack.last().is_some_and(Vec::is_empty) {
|
|
|
|
stack.pop();
|
|
|
|
parents.pop_back();
|
|
|
|
}
|
|
|
|
|
|
|
|
stack.last_mut().and_then(Vec::pop)
|
2023-07-02 16:06:54 +02:00
|
|
|
}
|