1309 lines
35 KiB
Rust
1309 lines
35 KiB
Rust
use std::{
|
|
fmt::{Display, Formatter},
|
|
str::FromStr,
|
|
};
|
|
|
|
use lru_cache::LruCache;
|
|
use ruma::{
|
|
api::{
|
|
client::{self, error::ErrorKind, space::SpaceHierarchyRoomsChunk},
|
|
federation::{
|
|
self,
|
|
space::{SpaceHierarchyChildSummary, SpaceHierarchyParentSummary},
|
|
},
|
|
},
|
|
events::{
|
|
room::{
|
|
avatar::RoomAvatarEventContent,
|
|
canonical_alias::RoomCanonicalAliasEventContent,
|
|
create::RoomCreateEventContent,
|
|
guest_access::{GuestAccess, RoomGuestAccessEventContent},
|
|
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent, RoomMembership},
|
|
topic::RoomTopicEventContent,
|
|
},
|
|
space::child::{HierarchySpaceChildEvent, SpaceChildEventContent},
|
|
StateEventType,
|
|
},
|
|
serde::Raw,
|
|
space::SpaceRoomJoinRule,
|
|
OwnedRoomId, OwnedServerName, RoomId, ServerName, UInt, UserId,
|
|
};
|
|
use tokio::sync::Mutex;
|
|
use tracing::{debug, error, warn};
|
|
|
|
use crate::{debug_info, server_is_ours, services, Error, Result};
|
|
|
|
pub struct CachedSpaceHierarchySummary {
|
|
summary: SpaceHierarchyParentSummary,
|
|
}
|
|
|
|
enum SummaryAccessibility {
|
|
Accessible(Box<SpaceHierarchyParentSummary>),
|
|
Inaccessible,
|
|
}
|
|
|
|
struct Arena {
|
|
nodes: Vec<Node>,
|
|
max_depth: usize,
|
|
first_untraversed: Option<NodeId>,
|
|
}
|
|
|
|
struct Node {
|
|
parent: Option<NodeId>,
|
|
// Next meaning:
|
|
// -->
|
|
// o o o o
|
|
next_sibling: Option<NodeId>,
|
|
// First meaning:
|
|
// |
|
|
// v
|
|
// o o o o
|
|
first_child: Option<NodeId>,
|
|
room_id: OwnedRoomId,
|
|
via: Vec<OwnedServerName>,
|
|
traversed: bool,
|
|
}
|
|
|
|
#[derive(Clone, Copy, PartialEq, Debug, PartialOrd)]
|
|
struct NodeId {
|
|
index: usize,
|
|
}
|
|
|
|
impl Arena {
|
|
/// Checks if a given node is traversed
|
|
fn traversed(&self, id: NodeId) -> Option<bool> { Some(self.get(id)?.traversed) }
|
|
|
|
/// Gets the previous sibling of a given node
|
|
fn next_sibling(&self, id: NodeId) -> Option<NodeId> { self.get(id)?.next_sibling }
|
|
|
|
/// Gets the parent of a given node
|
|
fn parent(&self, id: NodeId) -> Option<NodeId> { self.get(id)?.parent }
|
|
|
|
/// Gets the last child of a given node
|
|
fn first_child(&self, id: NodeId) -> Option<NodeId> { self.get(id)?.first_child }
|
|
|
|
/// Sets traversed to true for a given node
|
|
fn traverse(&mut self, id: NodeId) { self.nodes[id.index].traversed = true; }
|
|
|
|
/// Gets the node of a given id
|
|
fn get(&self, id: NodeId) -> Option<&Node> { self.nodes.get(id.index) }
|
|
|
|
/// Gets a mutable reference of a node of a given id
|
|
fn get_mut(&mut self, id: NodeId) -> Option<&mut Node> { self.nodes.get_mut(id.index) }
|
|
|
|
/// Returns the first untraversed node, marking it as traversed in the
|
|
/// process
|
|
fn first_untraversed(&mut self) -> Option<NodeId> {
|
|
if self.nodes.is_empty() {
|
|
None
|
|
} else if let Some(untraversed) = self.first_untraversed {
|
|
let mut current = untraversed;
|
|
|
|
self.traverse(untraversed);
|
|
|
|
// Possible paths:
|
|
// 1) Next child exists, and hence is not traversed
|
|
// 2) Next child does not exist, so go to the parent, then repeat
|
|
// 3) If both the parent and child do not exist, then we have just traversed the
|
|
// whole space tree.
|
|
//
|
|
// You should only ever encounter a traversed node when going up through parents
|
|
while let Some(true) = self.traversed(current) {
|
|
if let Some(next) = self.next_sibling(current) {
|
|
current = next;
|
|
} else if let Some(parent) = self.parent(current) {
|
|
current = parent;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Traverses down the children until it reaches one without children
|
|
while let Some(child) = self.first_child(current) {
|
|
current = child;
|
|
}
|
|
|
|
if self.traversed(current)? {
|
|
self.first_untraversed = None;
|
|
} else {
|
|
self.first_untraversed = Some(current);
|
|
}
|
|
|
|
Some(untraversed)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Adds all the given nodes as children of the parent node
|
|
fn push(&mut self, parent: NodeId, mut children: Vec<(OwnedRoomId, Vec<OwnedServerName>)>) {
|
|
if children.is_empty() {
|
|
self.traverse(parent);
|
|
} else if self.nodes.get(parent.index).is_some() {
|
|
let mut parents = vec![(
|
|
parent,
|
|
self.get(parent)
|
|
.expect("It is some, as above")
|
|
.room_id
|
|
// Cloning cause otherwise when iterating over the parents, below, there would
|
|
// be a mutable and immutable reference to self.nodes
|
|
.clone(),
|
|
)];
|
|
|
|
while let Some(parent) = self.parent(parents.last().expect("Has at least one value, as above").0) {
|
|
parents.push((
|
|
parent,
|
|
self.get(parent)
|
|
.expect("It is some, as above")
|
|
.room_id
|
|
.clone(),
|
|
));
|
|
}
|
|
|
|
// If at max_depth, don't add new rooms
|
|
if self.max_depth < parents.len() {
|
|
return;
|
|
}
|
|
|
|
children.reverse();
|
|
|
|
let mut next_id = None;
|
|
|
|
for (child, via) in children {
|
|
// Prevent adding a child which is a parent (recursion)
|
|
if !parents.iter().any(|parent| parent.1 == child) {
|
|
self.nodes.push(Node {
|
|
parent: Some(parent),
|
|
next_sibling: next_id,
|
|
first_child: None,
|
|
room_id: child,
|
|
traversed: false,
|
|
via,
|
|
});
|
|
|
|
next_id = Some(NodeId {
|
|
index: self.nodes.len() - 1,
|
|
});
|
|
}
|
|
}
|
|
|
|
if self.first_untraversed.is_none()
|
|
|| parent
|
|
>= self
|
|
.first_untraversed
|
|
.expect("Should have already continued if none")
|
|
{
|
|
self.first_untraversed = next_id;
|
|
}
|
|
|
|
self.traverse(parent);
|
|
|
|
// This is done as if we use an if-let above, we cannot reference self.nodes
|
|
// above as then we would have multiple mutable references
|
|
let node = self
|
|
.get_mut(parent)
|
|
.expect("Must be some, as inside this block");
|
|
|
|
node.first_child = next_id;
|
|
}
|
|
}
|
|
|
|
fn new(root: OwnedRoomId, max_depth: usize) -> Self {
|
|
let zero_depth = max_depth == 0;
|
|
|
|
Arena {
|
|
nodes: vec![Node {
|
|
parent: None,
|
|
next_sibling: None,
|
|
first_child: None,
|
|
room_id: root,
|
|
traversed: zero_depth,
|
|
via: vec![],
|
|
}],
|
|
max_depth,
|
|
first_untraversed: if zero_depth {
|
|
None
|
|
} else {
|
|
Some(NodeId {
|
|
index: 0,
|
|
})
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: perhaps use some better form of token rather than just room count
|
|
#[derive(Debug, PartialEq)]
|
|
pub struct PagnationToken {
|
|
pub skip: UInt,
|
|
pub limit: UInt,
|
|
pub max_depth: UInt,
|
|
pub suggested_only: bool,
|
|
}
|
|
|
|
impl FromStr for PagnationToken {
|
|
type Err = Error;
|
|
|
|
fn from_str(value: &str) -> Result<Self> {
|
|
let mut values = value.split('_');
|
|
|
|
let mut pag_tok = || {
|
|
Some(PagnationToken {
|
|
skip: UInt::from_str(values.next()?).ok()?,
|
|
limit: UInt::from_str(values.next()?).ok()?,
|
|
max_depth: UInt::from_str(values.next()?).ok()?,
|
|
suggested_only: {
|
|
let slice = values.next()?;
|
|
|
|
if values.next().is_none() {
|
|
if slice == "true" {
|
|
true
|
|
} else if slice == "false" {
|
|
false
|
|
} else {
|
|
None?
|
|
}
|
|
} else {
|
|
None?
|
|
}
|
|
},
|
|
})
|
|
};
|
|
|
|
if let Some(token) = pag_tok() {
|
|
Ok(token)
|
|
} else {
|
|
Err(Error::BadRequest(ErrorKind::InvalidParam, "invalid token"))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for PagnationToken {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}_{}_{}_{}", self.skip, self.limit, self.max_depth, self.suggested_only)
|
|
}
|
|
}
|
|
|
|
/// Identifier used to check if rooms are accessible
|
|
///
|
|
/// None is used if you want to return the room, no matter if accessible or not
|
|
enum Identifier<'a> {
|
|
UserId(&'a UserId),
|
|
ServerName(&'a ServerName),
|
|
None,
|
|
}
|
|
|
|
pub struct Service {
|
|
pub roomid_spacehierarchy_cache: Mutex<LruCache<OwnedRoomId, Option<CachedSpaceHierarchySummary>>>,
|
|
}
|
|
|
|
// Here because cannot implement `From` across ruma-federation-api and
|
|
// ruma-client-api types
|
|
impl From<CachedSpaceHierarchySummary> for SpaceHierarchyRoomsChunk {
|
|
fn from(value: CachedSpaceHierarchySummary) -> Self {
|
|
let SpaceHierarchyParentSummary {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id,
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
children_state,
|
|
..
|
|
} = value.summary;
|
|
|
|
SpaceHierarchyRoomsChunk {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id,
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
children_state,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Service {
|
|
///Gets the response for the space hierarchy over federation request
|
|
///
|
|
///Panics if the room does not exist, so a check if the room exists should
|
|
/// be done
|
|
pub async fn get_federation_hierarchy(
|
|
&self, room_id: &RoomId, server_name: &ServerName, suggested_only: bool,
|
|
) -> Result<federation::space::get_hierarchy::v1::Response> {
|
|
match self
|
|
.get_summary_and_children_local(&room_id.to_owned(), Identifier::None)
|
|
.await?
|
|
{
|
|
Some(SummaryAccessibility::Accessible(room)) => {
|
|
let mut children = Vec::new();
|
|
let mut inaccessible_children = Vec::new();
|
|
|
|
for (child, _via) in get_parent_children_via(&room, suggested_only) {
|
|
match self
|
|
.get_summary_and_children_local(&child, Identifier::ServerName(server_name))
|
|
.await?
|
|
{
|
|
Some(SummaryAccessibility::Accessible(summary)) => {
|
|
children.push((*summary).into());
|
|
},
|
|
Some(SummaryAccessibility::Inaccessible) => {
|
|
inaccessible_children.push(child);
|
|
},
|
|
None => (),
|
|
}
|
|
}
|
|
|
|
Ok(federation::space::get_hierarchy::v1::Response {
|
|
room: *room,
|
|
children,
|
|
inaccessible_children,
|
|
})
|
|
},
|
|
Some(SummaryAccessibility::Inaccessible) => {
|
|
Err(Error::BadRequest(ErrorKind::NotFound, "The requested room is inaccessible"))
|
|
},
|
|
None => Err(Error::BadRequest(ErrorKind::NotFound, "The requested room was not found")),
|
|
}
|
|
}
|
|
|
|
async fn get_summary_and_children_local(
|
|
&self, current_room: &OwnedRoomId, identifier: Identifier<'_>,
|
|
) -> Result<Option<SummaryAccessibility>> {
|
|
if let Some(cached) = self
|
|
.roomid_spacehierarchy_cache
|
|
.lock()
|
|
.await
|
|
.get_mut(¤t_room.to_owned())
|
|
.as_ref()
|
|
{
|
|
return Ok(if let Some(cached) = cached {
|
|
if is_accessable_child(
|
|
current_room,
|
|
&cached.summary.join_rule,
|
|
&identifier,
|
|
&cached.summary.allowed_room_ids,
|
|
)? {
|
|
Some(SummaryAccessibility::Accessible(Box::new(cached.summary.clone())))
|
|
} else {
|
|
Some(SummaryAccessibility::Inaccessible)
|
|
}
|
|
} else {
|
|
None
|
|
});
|
|
}
|
|
|
|
Ok(
|
|
if let Some(children_pdus) = get_stripped_space_child_events(current_room).await? {
|
|
let summary = Self::get_room_summary(current_room, children_pdus, &identifier);
|
|
if let Ok(summary) = summary {
|
|
self.roomid_spacehierarchy_cache.lock().await.insert(
|
|
current_room.clone(),
|
|
Some(CachedSpaceHierarchySummary {
|
|
summary: summary.clone(),
|
|
}),
|
|
);
|
|
|
|
Some(SummaryAccessibility::Accessible(Box::new(summary)))
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
},
|
|
)
|
|
}
|
|
|
|
async fn get_summary_and_children_federation(
|
|
&self, current_room: &OwnedRoomId, suggested_only: bool, user_id: &UserId, via: &[OwnedServerName],
|
|
) -> Result<Option<SummaryAccessibility>> {
|
|
debug_info!("servers via for federation hierarchy: {via:?}");
|
|
|
|
for server in via {
|
|
debug_info!("Asking {server} for /hierarchy");
|
|
if let Ok(response) = services()
|
|
.sending
|
|
.send_federation_request(
|
|
server,
|
|
federation::space::get_hierarchy::v1::Request {
|
|
room_id: current_room.to_owned(),
|
|
suggested_only,
|
|
},
|
|
)
|
|
.await
|
|
{
|
|
debug_info!("Got response from {server} for /hierarchy\n{response:?}");
|
|
let summary = response.room.clone();
|
|
|
|
self.roomid_spacehierarchy_cache.lock().await.insert(
|
|
current_room.clone(),
|
|
Some(CachedSpaceHierarchySummary {
|
|
summary: summary.clone(),
|
|
}),
|
|
);
|
|
|
|
for child in response.children {
|
|
let mut guard = self.roomid_spacehierarchy_cache.lock().await;
|
|
if !guard.contains_key(current_room) {
|
|
guard.insert(
|
|
current_room.clone(),
|
|
Some(CachedSpaceHierarchySummary {
|
|
summary: {
|
|
let SpaceHierarchyChildSummary {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id,
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
allowed_room_ids,
|
|
} = child;
|
|
|
|
SpaceHierarchyParentSummary {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id: room_id.clone(),
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
children_state: get_stripped_space_child_events(&room_id).await?.unwrap(),
|
|
allowed_room_ids,
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
if is_accessable_child(
|
|
current_room,
|
|
&response.room.join_rule,
|
|
&Identifier::UserId(user_id),
|
|
&response.room.allowed_room_ids,
|
|
)? {
|
|
return Ok(Some(SummaryAccessibility::Accessible(Box::new(summary.clone()))));
|
|
}
|
|
|
|
return Ok(Some(SummaryAccessibility::Inaccessible));
|
|
}
|
|
|
|
self.roomid_spacehierarchy_cache
|
|
.lock()
|
|
.await
|
|
.insert(current_room.clone(), None);
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
async fn get_summary_and_children_client(
|
|
&self, current_room: &OwnedRoomId, suggested_only: bool, user_id: &UserId, via: &[OwnedServerName],
|
|
) -> Result<Option<SummaryAccessibility>> {
|
|
if let Ok(Some(response)) = self
|
|
.get_summary_and_children_local(current_room, Identifier::UserId(user_id))
|
|
.await
|
|
{
|
|
Ok(Some(response))
|
|
} else {
|
|
self.get_summary_and_children_federation(current_room, suggested_only, user_id, via)
|
|
.await
|
|
}
|
|
}
|
|
|
|
fn get_room_summary(
|
|
current_room: &OwnedRoomId, children_state: Vec<Raw<HierarchySpaceChildEvent>>, identifier: &Identifier<'_>,
|
|
) -> Result<SpaceHierarchyParentSummary, Error> {
|
|
let room_id: &RoomId = current_room;
|
|
|
|
let 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| c.join_rule)
|
|
.map_err(|e| {
|
|
error!("Invalid room join rule event in database: {}", e);
|
|
Error::BadDatabase("Invalid room join rule event in database.")
|
|
})
|
|
})
|
|
.transpose()?
|
|
.unwrap_or(JoinRule::Invite);
|
|
|
|
let allowed_room_ids = allowed_room_ids(join_rule.clone());
|
|
|
|
if !is_accessable_child(current_room, &join_rule.clone().into(), identifier, &allowed_room_ids)? {
|
|
debug!("User is not allowed to see room {room_id}");
|
|
// This error will be caught later
|
|
return Err(Error::BadRequest(ErrorKind::forbidden(), "User is not allowed to see the room"));
|
|
}
|
|
|
|
let join_rule = join_rule.into();
|
|
|
|
Ok(SpaceHierarchyParentSummary {
|
|
canonical_alias: services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(room_id, &StateEventType::RoomCanonicalAlias, "")?
|
|
.map_or(Ok(None), |s| {
|
|
serde_json::from_str(s.content.get())
|
|
.map(|c: RoomCanonicalAliasEventContent| c.alias)
|
|
.map_err(|_| Error::bad_database("Invalid canonical alias event in database."))
|
|
})?,
|
|
name: services().rooms.state_accessor.get_name(room_id)?,
|
|
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"),
|
|
room_id: room_id.to_owned(),
|
|
topic: services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(room_id, &StateEventType::RoomTopic, "")?
|
|
.map_or(Ok(None), |s| {
|
|
serde_json::from_str(s.content.get())
|
|
.map(|c: RoomTopicEventContent| Some(c.topic))
|
|
.map_err(|_| {
|
|
error!("Invalid room topic event in database for room {}", room_id);
|
|
Error::bad_database("Invalid room topic event in database.")
|
|
})
|
|
})
|
|
.unwrap_or(None),
|
|
world_readable: services().rooms.state_accessor.is_world_readable(room_id)?,
|
|
guest_can_join: guest_can_join(room_id)?,
|
|
avatar_url: services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(room_id, &StateEventType::RoomAvatar, "")?
|
|
.map(|s| {
|
|
serde_json::from_str(s.content.get())
|
|
.map(|c: RoomAvatarEventContent| c.url)
|
|
.map_err(|_| Error::bad_database("Invalid room avatar event in database."))
|
|
})
|
|
.transpose()?
|
|
// url is now an Option<String> so we must flatten
|
|
.flatten(),
|
|
join_rule,
|
|
room_type: services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(room_id, &StateEventType::RoomCreate, "")?
|
|
.map(|s| {
|
|
serde_json::from_str::<RoomCreateEventContent>(s.content.get()).map_err(|e| {
|
|
error!("Invalid room create event in database: {}", e);
|
|
Error::BadDatabase("Invalid room create event in database.")
|
|
})
|
|
})
|
|
.transpose()?
|
|
.and_then(|e| e.room_type),
|
|
children_state,
|
|
allowed_room_ids,
|
|
})
|
|
}
|
|
|
|
// TODO: make this a lot less messy
|
|
pub async fn get_client_hierarchy(
|
|
&self, sender_user: &UserId, room_id: &RoomId, limit: usize, skip: usize, max_depth: usize,
|
|
suggested_only: bool,
|
|
) -> Result<client::space::get_hierarchy::v1::Response> {
|
|
// try to find more servers to fetch hierachy from if the only
|
|
// choice is the room ID's server name (usually dead)
|
|
//
|
|
// all spaces are normal rooms, so they should always have at least
|
|
// 1 admin in it which has a far higher chance of their server still
|
|
// being alive
|
|
let power_levels: ruma::events::room::power_levels::RoomPowerLevelsEventContent = services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(room_id, &StateEventType::RoomPowerLevels, "")?
|
|
.map(|ev| {
|
|
serde_json::from_str(ev.content.get())
|
|
.map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
|
|
})
|
|
.transpose()?
|
|
.unwrap_or_default();
|
|
|
|
// add server names of the list of admins in the room for backfill server
|
|
let mut via = power_levels
|
|
.users
|
|
.iter()
|
|
.filter(|(_, level)| **level > power_levels.users_default)
|
|
.map(|(user_id, _)| user_id.server_name())
|
|
.filter(|server| !server_is_ours(server))
|
|
.map(ToOwned::to_owned)
|
|
.collect::<Vec<_>>();
|
|
|
|
if let Some(server_name) = room_id.server_name() {
|
|
via.push(server_name.to_owned());
|
|
}
|
|
|
|
debug_info!("servers via for hierarchy: {via:?}");
|
|
|
|
match self
|
|
.get_summary_and_children_client(&room_id.to_owned(), suggested_only, sender_user, &via)
|
|
.await?
|
|
{
|
|
Some(SummaryAccessibility::Accessible(summary)) => {
|
|
let mut left_to_skip = skip;
|
|
let mut arena = Arena::new(summary.room_id.clone(), max_depth);
|
|
|
|
let mut results = Vec::new();
|
|
let root = arena
|
|
.first_untraversed()
|
|
.expect("The node just added is not traversed");
|
|
|
|
arena.push(root, get_parent_children_via(&summary, suggested_only));
|
|
if left_to_skip > 0 {
|
|
left_to_skip -= 1;
|
|
} else {
|
|
results.push(summary_to_chunk(*summary.clone()));
|
|
}
|
|
|
|
while let Some(current_room) = arena.first_untraversed() {
|
|
if limit > results.len() {
|
|
let node = arena
|
|
.get(current_room)
|
|
.expect("We added this node, it must exist");
|
|
if let Some(SummaryAccessibility::Accessible(summary)) = self
|
|
.get_summary_and_children_client(&node.room_id, suggested_only, sender_user, &node.via)
|
|
.await?
|
|
{
|
|
let children = get_parent_children_via(&summary, suggested_only);
|
|
arena.push(current_room, children);
|
|
|
|
if left_to_skip > 0 {
|
|
left_to_skip -= 1;
|
|
} else {
|
|
results.push(summary_to_chunk(*summary.clone()));
|
|
}
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
Ok(client::space::get_hierarchy::v1::Response {
|
|
next_batch: if results.len() < limit {
|
|
None
|
|
} else {
|
|
let skip = UInt::new((skip + limit) as u64);
|
|
|
|
skip.map(|skip| {
|
|
PagnationToken {
|
|
skip,
|
|
limit: UInt::new(max_depth as u64)
|
|
.expect("When sent in request it must have been valid UInt"),
|
|
max_depth: UInt::new(max_depth as u64)
|
|
.expect("When sent in request it must have been valid UInt"),
|
|
suggested_only,
|
|
}
|
|
.to_string()
|
|
})
|
|
},
|
|
rooms: results,
|
|
})
|
|
},
|
|
Some(SummaryAccessibility::Inaccessible) => {
|
|
Err(Error::BadRequest(ErrorKind::forbidden(), "The requested room is inaccessible"))
|
|
},
|
|
None => Err(Error::BadRequest(ErrorKind::forbidden(), "The requested room was not found")),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Simply returns the stripped m.space.child events of a room
|
|
async fn get_stripped_space_child_events(
|
|
room_id: &RoomId,
|
|
) -> Result<Option<Vec<Raw<HierarchySpaceChildEvent>>>, Error> {
|
|
if let Some(current_shortstatehash) = services().rooms.state.get_room_shortstatehash(room_id)? {
|
|
let state = services()
|
|
.rooms
|
|
.state_accessor
|
|
.state_full_ids(current_shortstatehash)
|
|
.await?;
|
|
let mut children_pdus = Vec::new();
|
|
for (key, id) in state {
|
|
let (event_type, state_key) = services().rooms.short.get_statekey_from_short(key)?;
|
|
if event_type != StateEventType::SpaceChild {
|
|
continue;
|
|
}
|
|
|
|
let pdu = services()
|
|
.rooms
|
|
.timeline
|
|
.get_pdu(&id)?
|
|
.ok_or_else(|| Error::bad_database("Event in space state not found"))?;
|
|
|
|
if serde_json::from_str::<SpaceChildEventContent>(pdu.content.get())
|
|
.ok()
|
|
.map(|c| c.via)
|
|
.map_or(true, |v| v.is_empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if OwnedRoomId::try_from(state_key).is_ok() {
|
|
children_pdus.push(pdu.to_stripped_spacechild_state_event());
|
|
}
|
|
}
|
|
Ok(Some(children_pdus))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
|
|
/// With the given identifier, checks if a room is accessable
|
|
fn is_accessable_child(
|
|
current_room: &OwnedRoomId, join_rule: &SpaceRoomJoinRule, identifier: &Identifier<'_>,
|
|
allowed_room_ids: &Vec<OwnedRoomId>,
|
|
) -> Result<bool, Error> {
|
|
is_accessable_child_recurse(current_room, join_rule, identifier, allowed_room_ids, 0)
|
|
}
|
|
|
|
fn is_accessable_child_recurse(
|
|
current_room: &OwnedRoomId, join_rule: &SpaceRoomJoinRule, identifier: &Identifier<'_>,
|
|
allowed_room_ids: &Vec<OwnedRoomId>, recurse_num: usize,
|
|
) -> Result<bool, Error> {
|
|
// Set limit at 10, as we cannot keep going up parents forever
|
|
// and it is very unlikely to have 10 space parents
|
|
if recurse_num < 10 {
|
|
match identifier {
|
|
Identifier::ServerName(server_name) => {
|
|
let room_id: &RoomId = current_room;
|
|
|
|
// Checks if ACLs allow for the server to participate
|
|
if services()
|
|
.rooms
|
|
.event_handler
|
|
.acl_check(server_name, room_id)
|
|
.is_err()
|
|
{
|
|
return Ok(false);
|
|
}
|
|
},
|
|
Identifier::UserId(user_id) => {
|
|
if services()
|
|
.rooms
|
|
.state_cache
|
|
.is_joined(user_id, current_room)?
|
|
|| services()
|
|
.rooms
|
|
.state_cache
|
|
.is_invited(user_id, current_room)?
|
|
{
|
|
return Ok(true);
|
|
}
|
|
},
|
|
Identifier::None => (),
|
|
} // Takes care of joinrules
|
|
Ok(match join_rule {
|
|
SpaceRoomJoinRule::Restricted => {
|
|
for room in allowed_room_ids {
|
|
if let Ok((join_rule, allowed_room_ids)) = get_join_rule(room) {
|
|
if let Ok(true) = is_accessable_child_recurse(
|
|
room,
|
|
&join_rule,
|
|
identifier,
|
|
&allowed_room_ids,
|
|
recurse_num + 1,
|
|
) {
|
|
return Ok(true);
|
|
}
|
|
}
|
|
}
|
|
false
|
|
},
|
|
SpaceRoomJoinRule::Public | SpaceRoomJoinRule::Knock | SpaceRoomJoinRule::KnockRestricted => true,
|
|
// Custom join rules, Invite, or Private
|
|
_ => false,
|
|
})
|
|
} else {
|
|
// If you need to go up 10 parents, we just assume it is inaccessable
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
/// Checks if guests are able to join a given room
|
|
fn guest_can_join(room_id: &RoomId) -> Result<bool, Error> {
|
|
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."))
|
|
})
|
|
}
|
|
|
|
/// Returns the join rule for a given room
|
|
fn get_join_rule(current_room: &RoomId) -> Result<(SpaceRoomJoinRule, Vec<OwnedRoomId>), Error> {
|
|
Ok(services()
|
|
.rooms
|
|
.state_accessor
|
|
.room_state_get(current_room, &StateEventType::RoomJoinRules, "")?
|
|
.map(|s| {
|
|
serde_json::from_str(s.content.get())
|
|
.map(|c: RoomJoinRulesEventContent| (c.join_rule.clone().into(), allowed_room_ids(c.join_rule)))
|
|
.map_err(|e| {
|
|
error!("Invalid room join rule event in database: {}", e);
|
|
Error::BadDatabase("Invalid room join rule event in database.")
|
|
})
|
|
})
|
|
.transpose()?
|
|
.unwrap_or((SpaceRoomJoinRule::Invite, vec![])))
|
|
}
|
|
|
|
// Here because cannot implement `From` across ruma-federation-api and
|
|
// ruma-client-api types
|
|
fn summary_to_chunk(summary: SpaceHierarchyParentSummary) -> SpaceHierarchyRoomsChunk {
|
|
let SpaceHierarchyParentSummary {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id,
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
children_state,
|
|
..
|
|
} = summary;
|
|
|
|
SpaceHierarchyRoomsChunk {
|
|
canonical_alias,
|
|
name,
|
|
num_joined_members,
|
|
room_id,
|
|
topic,
|
|
world_readable,
|
|
guest_can_join,
|
|
avatar_url,
|
|
join_rule,
|
|
room_type,
|
|
children_state,
|
|
}
|
|
}
|
|
|
|
/// Returns an empty vec if not a restricted room
|
|
fn allowed_room_ids(join_rule: JoinRule) -> Vec<OwnedRoomId> {
|
|
let mut room_ids = vec![];
|
|
if let JoinRule::Restricted(r) | JoinRule::KnockRestricted(r) = join_rule {
|
|
for rule in r.allow {
|
|
if let AllowRule::RoomMembership(RoomMembership {
|
|
room_id: membership,
|
|
}) = rule
|
|
{
|
|
room_ids.push(membership.clone());
|
|
}
|
|
}
|
|
}
|
|
room_ids
|
|
}
|
|
|
|
/// Returns the children of a SpaceHierarchyParentSummary, making use of the
|
|
/// children_state field
|
|
fn get_parent_children_via(
|
|
parent: &SpaceHierarchyParentSummary, suggested_only: bool,
|
|
) -> Vec<(OwnedRoomId, Vec<OwnedServerName>)> {
|
|
parent
|
|
.children_state
|
|
.iter()
|
|
.filter_map(|raw_ce| {
|
|
raw_ce.deserialize().map_or(None, |ce| {
|
|
if suggested_only && !ce.content.suggested {
|
|
None
|
|
} else {
|
|
Some((ce.state_key, ce.content.via))
|
|
}
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use ruma::{
|
|
api::federation::space::SpaceHierarchyParentSummaryInit, events::room::join_rules::Restricted, owned_room_id,
|
|
owned_server_name,
|
|
};
|
|
|
|
use super::*;
|
|
|
|
fn first(arena: &mut Arena, room_id: &OwnedRoomId) {
|
|
let first_untrav = arena.first_untraversed().unwrap();
|
|
|
|
assert_eq!(arena.get(first_untrav).unwrap().room_id, *room_id);
|
|
}
|
|
|
|
#[test]
|
|
fn zero_depth() {
|
|
let mut arena = Arena::new(owned_room_id!("!foo:example.org"), 0);
|
|
|
|
assert_eq!(arena.first_untraversed(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn two_depth() {
|
|
let mut arena = Arena::new(owned_room_id!("!root:example.org"), 2);
|
|
|
|
let root = arena.first_untraversed().unwrap();
|
|
arena.push(
|
|
root,
|
|
vec![
|
|
(owned_room_id!("!subspace1:example.org"), vec![]),
|
|
(owned_room_id!("!subspace2:example.org"), vec![]),
|
|
(owned_room_id!("!foo:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
let subspace1 = arena.first_untraversed().unwrap();
|
|
let subspace2 = arena.first_untraversed().unwrap();
|
|
|
|
arena.push(
|
|
subspace1,
|
|
vec![
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
(owned_room_id!("!room2:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room2:example.org"));
|
|
|
|
arena.push(
|
|
subspace2,
|
|
vec![
|
|
(owned_room_id!("!room3:example.org"), vec![]),
|
|
(owned_room_id!("!room4:example.org"), vec![]),
|
|
],
|
|
);
|
|
first(&mut arena, &owned_room_id!("!room3:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room4:example.org"));
|
|
|
|
let foo_node = NodeId {
|
|
index: 1,
|
|
};
|
|
|
|
assert_eq!(arena.first_untraversed(), Some(foo_node));
|
|
assert_eq!(
|
|
arena.get(foo_node).map(|node| node.room_id.clone()),
|
|
Some(owned_room_id!("!foo:example.org"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty_push() {
|
|
let mut arena = Arena::new(owned_room_id!("!root:example.org"), 5);
|
|
|
|
let root = arena.first_untraversed().unwrap();
|
|
arena.push(
|
|
root,
|
|
vec![
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
(owned_room_id!("!room2:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
let room1 = arena.first_untraversed().unwrap();
|
|
arena.push(room1, vec![]);
|
|
|
|
first(&mut arena, &owned_room_id!("!room2:example.org"));
|
|
assert!(arena.first_untraversed().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn beyond_max_depth() {
|
|
let mut arena = Arena::new(owned_room_id!("!root:example.org"), 0);
|
|
|
|
let root = NodeId {
|
|
index: 0,
|
|
};
|
|
|
|
arena.push(root, vec![(owned_room_id!("!too_deep:example.org"), vec![])]);
|
|
|
|
assert_eq!(arena.first_child(root), None);
|
|
assert_eq!(arena.nodes.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn order_check() {
|
|
let mut arena = Arena::new(owned_room_id!("!root:example.org"), 3);
|
|
|
|
let root = arena.first_untraversed().unwrap();
|
|
arena.push(
|
|
root,
|
|
vec![
|
|
(owned_room_id!("!subspace1:example.org"), vec![]),
|
|
(owned_room_id!("!subspace2:example.org"), vec![]),
|
|
(owned_room_id!("!foo:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
let subspace1 = arena.first_untraversed().unwrap();
|
|
arena.push(
|
|
subspace1,
|
|
vec![
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
(owned_room_id!("!room3:example.org"), vec![]),
|
|
(owned_room_id!("!room5:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room3:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room5:example.org"));
|
|
|
|
let subspace2 = arena.first_untraversed().unwrap();
|
|
|
|
assert_eq!(arena.get(subspace2).unwrap().room_id, owned_room_id!("!subspace2:example.org"));
|
|
|
|
arena.push(
|
|
subspace2,
|
|
vec![
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
(owned_room_id!("!room2:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room2:example.org"));
|
|
first(&mut arena, &owned_room_id!("!foo:example.org"));
|
|
|
|
assert_eq!(arena.first_untraversed(), None);
|
|
}
|
|
|
|
#[test]
|
|
fn get_summary_children() {
|
|
let summary: SpaceHierarchyParentSummary = SpaceHierarchyParentSummaryInit {
|
|
num_joined_members: UInt::from(1_u32),
|
|
room_id: owned_room_id!("!root:example.org"),
|
|
world_readable: true,
|
|
guest_can_join: true,
|
|
join_rule: SpaceRoomJoinRule::Public,
|
|
children_state: vec![
|
|
serde_json::from_str(
|
|
r#"{
|
|
"content": {
|
|
"via": [
|
|
"example.org"
|
|
],
|
|
"suggested": false
|
|
},
|
|
"origin_server_ts": 1629413349153,
|
|
"sender": "@alice:example.org",
|
|
"state_key": "!foo:example.org",
|
|
"type": "m.space.child"
|
|
}"#,
|
|
)
|
|
.unwrap(),
|
|
serde_json::from_str(
|
|
r#"{
|
|
"content": {
|
|
"via": [
|
|
"example.org"
|
|
],
|
|
"suggested": true
|
|
},
|
|
"origin_server_ts": 1629413349157,
|
|
"sender": "@alice:example.org",
|
|
"state_key": "!bar:example.org",
|
|
"type": "m.space.child"
|
|
}"#,
|
|
)
|
|
.unwrap(),
|
|
serde_json::from_str(
|
|
r#"{
|
|
"content": {
|
|
"via": [
|
|
"example.org"
|
|
]
|
|
},
|
|
"origin_server_ts": 1629413349160,
|
|
"sender": "@alice:example.org",
|
|
"state_key": "!baz:example.org",
|
|
"type": "m.space.child"
|
|
}"#,
|
|
)
|
|
.unwrap(),
|
|
],
|
|
allowed_room_ids: vec![],
|
|
}
|
|
.into();
|
|
|
|
assert_eq!(
|
|
get_parent_children_via(&summary, false),
|
|
vec![
|
|
(owned_room_id!("!foo:example.org"), vec![owned_server_name!("example.org")]),
|
|
(owned_room_id!("!bar:example.org"), vec![owned_server_name!("example.org")]),
|
|
(owned_room_id!("!baz:example.org"), vec![owned_server_name!("example.org")])
|
|
]
|
|
);
|
|
assert_eq!(
|
|
get_parent_children_via(&summary, true),
|
|
vec![(owned_room_id!("!bar:example.org"), vec![owned_server_name!("example.org")])]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn allowed_room_ids_from_join_rule() {
|
|
let restricted_join_rule = JoinRule::Restricted(Restricted {
|
|
allow: vec![
|
|
AllowRule::RoomMembership(RoomMembership {
|
|
room_id: owned_room_id!("!foo:example.org"),
|
|
}),
|
|
AllowRule::RoomMembership(RoomMembership {
|
|
room_id: owned_room_id!("!bar:example.org"),
|
|
}),
|
|
AllowRule::RoomMembership(RoomMembership {
|
|
room_id: owned_room_id!("!baz:example.org"),
|
|
}),
|
|
],
|
|
});
|
|
|
|
let invite_join_rule = JoinRule::Invite;
|
|
|
|
assert_eq!(
|
|
allowed_room_ids(restricted_join_rule),
|
|
vec![
|
|
owned_room_id!("!foo:example.org"),
|
|
owned_room_id!("!bar:example.org"),
|
|
owned_room_id!("!baz:example.org")
|
|
]
|
|
);
|
|
|
|
let empty_vec: Vec<OwnedRoomId> = vec![];
|
|
|
|
assert_eq!(allowed_room_ids(invite_join_rule), empty_vec);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_pagnation_tokens() {
|
|
fn token_is_err(token: &str) {
|
|
let token: Result<PagnationToken> = PagnationToken::from_str(token);
|
|
token.unwrap_err();
|
|
}
|
|
|
|
token_is_err("231_2_noabool");
|
|
token_is_err("");
|
|
token_is_err("111_3_");
|
|
token_is_err("foo_not_int");
|
|
token_is_err("11_4_true_");
|
|
token_is_err("___");
|
|
token_is_err("__false");
|
|
}
|
|
|
|
#[test]
|
|
fn valid_pagnation_tokens() {
|
|
assert_eq!(
|
|
PagnationToken {
|
|
skip: UInt::from(40_u32),
|
|
limit: UInt::from(20_u32),
|
|
max_depth: UInt::from(1_u32),
|
|
suggested_only: true
|
|
},
|
|
PagnationToken::from_str("40_20_1_true").unwrap()
|
|
);
|
|
|
|
assert_eq!(
|
|
PagnationToken {
|
|
skip: UInt::from(27645_u32),
|
|
limit: UInt::from(97_u32),
|
|
max_depth: UInt::from(10539_u32),
|
|
suggested_only: false
|
|
},
|
|
PagnationToken::from_str("27645_97_10539_false").unwrap()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn pagnation_token_to_string() {
|
|
assert_eq!(
|
|
PagnationToken {
|
|
skip: UInt::from(27645_u32),
|
|
limit: UInt::from(97_u32),
|
|
max_depth: UInt::from(9420_u32),
|
|
suggested_only: false
|
|
}
|
|
.to_string(),
|
|
"27645_97_9420_false"
|
|
);
|
|
|
|
assert_eq!(
|
|
PagnationToken {
|
|
skip: UInt::from(12_u32),
|
|
limit: UInt::from(3_u32),
|
|
max_depth: UInt::from(1_u32),
|
|
suggested_only: true
|
|
}
|
|
.to_string(),
|
|
"12_3_1_true"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn forbid_recursion() {
|
|
let mut arena = Arena::new(owned_room_id!("!root:example.org"), 5);
|
|
let root_node_id = arena.first_untraversed().unwrap();
|
|
|
|
arena.push(
|
|
root_node_id,
|
|
vec![
|
|
(owned_room_id!("!subspace1:example.org"), vec![]),
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
(owned_room_id!("!subspace2:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
let subspace1_node_id = arena.first_untraversed().unwrap();
|
|
arena.push(
|
|
subspace1_node_id,
|
|
vec![
|
|
(owned_room_id!("!subspace2:example.org"), vec![]),
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
let subspace2_node_id = arena.first_untraversed().unwrap();
|
|
// Here, both subspaces should be ignored and not added, as they are both
|
|
// parents of subspace2
|
|
arena.push(
|
|
subspace2_node_id,
|
|
vec![
|
|
(owned_room_id!("!subspace1:example.org"), vec![]),
|
|
(owned_room_id!("!subspace2:example.org"), vec![]),
|
|
(owned_room_id!("!room1:example.org"), vec![]),
|
|
],
|
|
);
|
|
|
|
assert_eq!(arena.nodes.len(), 7);
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!room1:example.org"));
|
|
first(&mut arena, &owned_room_id!("!subspace2:example.org"));
|
|
assert!(arena.first_untraversed().is_none());
|
|
}
|
|
}
|