diff --git a/src/api/client_server/space.rs b/src/api/client_server/space.rs index ac6139a5..0cbd6057 100644 --- a/src/api/client_server/space.rs +++ b/src/api/client_server/space.rs @@ -1,6 +1,11 @@ -use ruma::api::client::space::get_hierarchy; +use std::str::FromStr; -use crate::{services, Result, Ruma}; +use ruma::{ + api::client::{error::ErrorKind, space::get_hierarchy}, + UInt, +}; + +use crate::{service::rooms::spaces::PagnationToken, services, Error, Result, Ruma}; /// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy`` /// @@ -9,11 +14,32 @@ use crate::{services, Result, Ruma}; pub async fn get_hierarchy_route(body: Ruma) -> Result { let sender_user = body.sender_user.as_ref().expect("user is authenticated"); - let skip = body.from.as_ref().and_then(|s| s.parse::().ok()).unwrap_or(0); + let limit = body.limit.unwrap_or_else(|| UInt::from(10_u32)).min(UInt::from(100_u32)); - let limit = body.limit.map_or(10, u64::from).min(100) as usize; + let max_depth = body.max_depth.unwrap_or_else(|| UInt::from(3_u32)).min(UInt::from(10_u32)); - let max_depth = body.max_depth.map_or(3, u64::from).min(10) as usize + 1; // +1 to skip the space room itself + let key = body.from.as_ref().and_then(|s| PagnationToken::from_str(s).ok()); - services().rooms.spaces.get_hierarchy(sender_user, &body.room_id, limit, skip, max_depth, body.suggested_only).await + // 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", + )); + } + } + + services() + .rooms + .spaces + .get_client_hierarchy( + sender_user, + &body.room_id, + u64::from(limit) as usize, + key.map_or(0, |token| u64::from(token.skip) as usize), + u64::from(max_depth) as usize, + body.suggested_only, + ) + .await } diff --git a/src/api/server_server.rs b/src/api/server_server.rs index 5158ca45..4f28e271 100644 --- a/src/api/server_server.rs +++ b/src/api/server_server.rs @@ -28,6 +28,7 @@ use ruma::{ keys::{claim_keys, get_keys}, membership::{create_invite, create_join_event, prepare_join_event}, query::{get_profile_information, get_room_information}, + space::get_hierarchy, transactions::{ edu::{DeviceListUpdateContent, DirectDeviceContent, Edu, SigningKeyUpdateContent}, send_transaction_message, @@ -1734,6 +1735,20 @@ pub async fn well_known_server_route() -> Result { }))) } +/// # `GET /_matrix/federation/v1/hierarchy/{roomId}` +/// +/// Gets the space tree in a depth-first manner to locate child rooms of a given +/// space. +pub async fn get_hierarchy_route(body: Ruma) -> Result { + let sender_servername = body.sender_servername.as_ref().expect("server is authenticated"); + + if services().rooms.metadata.exists(&body.room_id)? { + services().rooms.spaces.get_federation_hierarchy(&body.room_id, sender_servername, body.suggested_only).await + } else { + Err(Error::BadRequest(ErrorKind::NotFound, "Room does not exist.")) + } +} + #[cfg(test)] mod tests { use super::{add_port_to_hostname, get_ip_with_port, FedDest}; diff --git a/src/database/key_value/rooms/state_cache.rs b/src/database/key_value/rooms/state_cache.rs index 6c19dbe8..1645c874 100644 --- a/src/database/key_value/rooms/state_cache.rs +++ b/src/database/key_value/rooms/state_cache.rs @@ -268,6 +268,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { })) } + /// Returns the number of users which are currently in a room #[tracing::instrument(skip(self))] fn room_joined_count(&self, room_id: &RoomId) -> Result> { self.roomid_joinedcount @@ -276,6 +277,7 @@ impl service::rooms::state_cache::Data for KeyValueDatabase { .transpose() } + /// Returns the number of users which are currently invited to a room #[tracing::instrument(skip(self))] fn room_invited_count(&self, room_id: &RoomId) -> Result> { self.roomid_invitedcount diff --git a/src/main.rs b/src/main.rs index ed163519..03c9bdf6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -739,6 +739,7 @@ fn routes() -> Router { .ruma_route(server_server::get_profile_information_route) .ruma_route(server_server::get_keys_route) .ruma_route(server_server::claim_keys_route) + .ruma_route(server_server::get_hierarchy_route) .route("/_matrix/client/r0/rooms/:room_id/initialSync", get(initial_sync)) .route("/_matrix/client/v3/rooms/:room_id/initialSync", get(initial_sync)) .route("/client/server.json", get(client_server::syncv3_client_server_json)) diff --git a/src/service/mod.rs b/src/service/mod.rs index 25413647..8d37061c 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -134,7 +134,7 @@ impl Services<'_> { db, }, spaces: rooms::spaces::Service { - roomid_spacechunk_cache: Mutex::new(LruCache::new( + roomid_spacehierarchy_cache: Mutex::new(LruCache::new( (100.0 * config.conduit_cache_capacity_modifier) as usize, )), }, @@ -175,7 +175,7 @@ impl Services<'_> { let user_visibility_cache = self.rooms.state_accessor.user_visibility_cache.lock().unwrap().len(); let stateinfo_cache = self.rooms.state_compressor.stateinfo_cache.lock().unwrap().len(); let lasttimelinecount_cache = self.rooms.timeline.lasttimelinecount_cache.lock().await.len(); - let roomid_spacechunk_cache = self.rooms.spaces.roomid_spacechunk_cache.lock().await.len(); + let roomid_spacehierarchy_cache = self.rooms.spaces.roomid_spacehierarchy_cache.lock().await.len(); format!( "\ @@ -184,7 +184,7 @@ server_visibility_cache: {server_visibility_cache} user_visibility_cache: {user_visibility_cache} stateinfo_cache: {stateinfo_cache} lasttimelinecount_cache: {lasttimelinecount_cache} -roomid_spacechunk_cache: {roomid_spacechunk_cache}" +roomid_spacehierarchy_cache: {roomid_spacehierarchy_cache}" ) } @@ -205,7 +205,7 @@ roomid_spacechunk_cache: {roomid_spacechunk_cache}" self.rooms.timeline.lasttimelinecount_cache.lock().await.clear(); } if amount > 5 { - self.rooms.spaces.roomid_spacechunk_cache.lock().await.clear(); + self.rooms.spaces.roomid_spacehierarchy_cache.lock().await.clear(); } } } diff --git a/src/service/rooms/spaces/mod.rs b/src/service/rooms/spaces/mod.rs index 4e7ffcca..20a50d71 100644 --- a/src/service/rooms/spaces/mod.rs +++ b/src/service/rooms/spaces/mod.rs @@ -1,13 +1,13 @@ -use std::sync::Arc; +use std::str::FromStr; use lru_cache::LruCache; use ruma::{ api::{ - client::{ - error::ErrorKind, - space::{get_hierarchy, SpaceHierarchyRoomsChunk}, + client::{self, error::ErrorKind, space::SpaceHierarchyRoomsChunk}, + federation::{ + self, + space::{SpaceHierarchyChildSummary, SpaceHierarchyParentSummary}, }, - federation, }, events::{ room::{ @@ -16,255 +16,503 @@ use ruma::{ create::RoomCreateEventContent, guest_access::{GuestAccess, RoomGuestAccessEventContent}, history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, - join_rules::{self, AllowRule, JoinRule, RoomJoinRulesEventContent}, + join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent, RoomMembership}, topic::RoomTopicEventContent, }, - space::child::SpaceChildEventContent, + space::child::{HierarchySpaceChildEvent, SpaceChildEventContent}, StateEventType, }, + serde::Raw, space::SpaceRoomJoinRule, - OwnedRoomId, RoomId, UserId, + OwnedRoomId, RoomId, ServerName, UInt, UserId, }; use tokio::sync::Mutex; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; -use crate::{services, Error, PduEvent, Result}; +use crate::{services, Error, Result}; -pub enum CachedJoinRule { - //Simplified(SpaceRoomJoinRule), - Full(JoinRule), +pub struct CachedSpaceHierarchySummary { + summary: SpaceHierarchyParentSummary, } -pub struct CachedSpaceChunk { - chunk: SpaceHierarchyRoomsChunk, - children: Vec, - join_rule: CachedJoinRule, +pub enum SummaryAccessibility { + Accessible(Box), + Inaccessible, +} + +pub struct Arena { + nodes: Vec, + max_depth: usize, + first_untraversed: Option, +} + +pub struct Node { + parent: Option, + // Next meaning: + // --> + // o o o o + next_sibling: Option, + // First meaning: + // | + // v + // o o o o + first_child: Option, + pub room_id: OwnedRoomId, + traversed: bool, +} + +#[derive(Clone, Copy, PartialEq, Debug, PartialOrd)] +pub struct NodeId { + index: usize, +} + +impl Arena { + /// Checks if a given node is traversed + fn traversed(&self, id: NodeId) -> Option { Some(self.get(id)?.traversed) } + + /// Gets the previous sibling of a given node + fn next_sibling(&self, id: NodeId) -> Option { self.get(id)?.next_sibling } + + /// Gets the parent of a given node + fn parent(&self, id: NodeId) -> Option { self.get(id)?.parent } + + /// Gets the last child of a given node + fn first_child(&self, id: NodeId) -> Option { 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 + pub fn first_untraversed(&mut self) -> Option { + 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 + pub fn push(&mut self, parent: NodeId, mut children: Vec) { + 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 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, + }); + + 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; + } + } + + pub 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, + }], + 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 { + 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 std::fmt::Display for PagnationToken { + fn fmt(&self, f: &mut std::fmt::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 +pub enum Identifier<'a> { + UserId(&'a UserId), + ServerName(&'a ServerName), + None, } pub struct Service { - pub roomid_spacechunk_cache: Mutex>>, + pub roomid_spacehierarchy_cache: Mutex>>, +} + +// Here because cannot implement `From` across ruma-federation-api and +// ruma-client-api types +impl From 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 { - pub async fn get_hierarchy( - &self, sender_user: &UserId, room_id: &RoomId, limit: usize, skip: usize, max_depth: usize, - suggested_only: bool, - ) -> Result { - let mut left_to_skip = skip; + ///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 { + match self.get_summary_and_children(&room_id.to_owned(), suggested_only, Identifier::None).await? { + Some(SummaryAccessibility::Accessible(room)) => { + let mut children = Vec::new(); + let mut inaccessible_children = Vec::new(); - let mut rooms_in_path = Vec::new(); - let mut stack = vec![vec![room_id.to_owned()]]; - let mut results = Vec::new(); + for child in get_parent_children(*room.clone(), suggested_only) { + match self + .get_summary_and_children(&child, suggested_only, Identifier::ServerName(server_name)) + .await? + { + Some(SummaryAccessibility::Accessible(summary)) => { + children.push((*summary).into()); + }, + Some(SummaryAccessibility::Inaccessible) => { + inaccessible_children.push(child); + }, + None => (), + } + } - while let Some(current_room) = { - while stack.last().map_or(false, Vec::is_empty) { - stack.pop(); - } - if !stack.is_empty() { - stack.last_mut().and_then(Vec::pop) + 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( + &self, current_room: &OwnedRoomId, suggested_only: bool, identifier: Identifier<'_>, + ) -> Result> { + 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 - } - } { - rooms_in_path.push(current_room.clone()); - if results.len() >= limit { - break; - } + }); + } - if let Some(cached) = self.roomid_spacechunk_cache.lock().await.get_mut(¤t_room.clone()).as_ref() { - if let Some(cached) = cached { - let allowed = match &cached.join_rule { - //CachedJoinRule::Simplified(s) => { - //self.handle_simplified_join_rule(s, sender_user, ¤t_room)? - //} - CachedJoinRule::Full(f) => self.handle_join_rule(f, sender_user, ¤t_room)?, - }; - if allowed { - if left_to_skip > 0 { - left_to_skip -= 1; - } else { - results.push(cached.chunk.clone()); - } - if rooms_in_path.len() < max_depth { - stack.push(cached.children.clone()); - } - } - } - continue; - } - - if let Some(current_shortstatehash) = services().rooms.state.get_room_shortstatehash(¤t_room)? { - let state = services().rooms.state_accessor.state_full_ids(current_shortstatehash).await?; - - let mut children_ids = Vec::new(); - 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::(pdu.content.get()) - .ok() - .map(|c| c.via) - .map_or(true, |v| v.is_empty()) - { - continue; - } - - if let Ok(room_id) = OwnedRoomId::try_from(state_key) { - children_ids.push(room_id); - children_pdus.push(pdu); - } - } - - // TODO: Sort children - children_ids.reverse(); - - let chunk = self.get_room_chunk(sender_user, ¤t_room, children_pdus).await; - if let Ok(chunk) = chunk { - if left_to_skip > 0 { - left_to_skip -= 1; - } else { - results.push(chunk.clone()); - } - let join_rule = services() - .rooms - .state_accessor - .room_state_get(¤t_room, &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); - - self.roomid_spacechunk_cache.lock().await.insert( + 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(CachedSpaceChunk { - chunk, - children: children_ids.clone(), - join_rule: CachedJoinRule::Full(join_rule), + Some(CachedSpaceHierarchySummary { + summary: summary.clone(), }), ); - } - if rooms_in_path.len() < max_depth { - stack.push(children_ids); + Some(SummaryAccessibility::Accessible(Box::new(summary))) + } else { + None } - } else if let Some(server) = current_room.server_name() { + // Federation requests should not request information from other + // servers + } else if let Identifier::UserId(_) = identifier { + let server = current_room.server_name().expect("Room IDs should always have a server name"); if server == services().globals.server_name() { - continue; + return Ok(None); } - if !results.is_empty() { - // Early return so the client can see some data already - break; - } - - debug!("Asking {server} for /hierarchy"); + 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.clone(), + room_id: current_room.to_owned(), suggested_only, }, ) .await { - debug!("Got response from {server} for /hierarchy\n{response:?}"); - let chunk = SpaceHierarchyRoomsChunk { - canonical_alias: response.room.canonical_alias, - name: response.room.name, - num_joined_members: response.room.num_joined_members, - room_id: response.room.room_id, - topic: response.room.topic, - world_readable: response.room.world_readable, - guest_can_join: response.room.guest_can_join, - avatar_url: response.room.avatar_url, - join_rule: response.room.join_rule.clone(), - room_type: response.room.room_type, - children_state: response.room.children_state, - }; - let children = response.children.iter().map(|c| c.room_id.clone()).collect::>(); + info!("Got response from {server} for /hierarchy\n{response:?}"); + let summary = response.room.clone(); - let join_rule = match response.room.join_rule { - SpaceRoomJoinRule::Invite => JoinRule::Invite, - SpaceRoomJoinRule::Knock => JoinRule::Knock, - SpaceRoomJoinRule::Private => JoinRule::Private, - SpaceRoomJoinRule::Restricted => JoinRule::Restricted(join_rules::Restricted { - allow: response.room.allowed_room_ids.into_iter().map(AllowRule::room_membership).collect(), + self.roomid_spacehierarchy_cache.lock().await.insert( + current_room.clone(), + Some(CachedSpaceHierarchySummary { + summary: summary.clone(), }), - SpaceRoomJoinRule::KnockRestricted => JoinRule::KnockRestricted(join_rules::Restricted { - allow: response.room.allowed_room_ids.into_iter().map(AllowRule::room_membership).collect(), - }), - SpaceRoomJoinRule::Public => JoinRule::Public, - _ => return Err(Error::BadServerResponse("Unknown join rule")), - }; - if self.handle_join_rule(&join_rule, sender_user, ¤t_room)? { - if left_to_skip > 0 { - left_to_skip -= 1; - } else { - results.push(chunk.clone()); - } - if rooms_in_path.len() < max_depth { - stack.push(children.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, + } + }, + }), + ); } } - - self.roomid_spacechunk_cache.lock().await.insert( - current_room.clone(), - Some(CachedSpaceChunk { - chunk, - children, - join_rule: CachedJoinRule::Full(join_rule), - }), - ); - - /* TODO: - for child in response.children { - roomid_spacechunk_cache.insert( - current_room.clone(), - CachedSpaceChunk { - chunk: child.chunk, - children, - join_rule, - }, - ); - } - */ + if is_accessable_child( + current_room, + &response.room.join_rule, + &identifier, + &response.room.allowed_room_ids, + )? { + Some(SummaryAccessibility::Accessible(Box::new(summary.clone()))) + } else { + Some(SummaryAccessibility::Inaccessible) + } } else { - self.roomid_spacechunk_cache.lock().await.insert(current_room.clone(), None); - } - } - } + self.roomid_spacehierarchy_cache.lock().await.insert(current_room.clone(), None); - Ok(get_hierarchy::v1::Response { - next_batch: if results.is_empty() { - None + None + } } else { - Some((skip + results.len()).to_string()) + None }, - rooms: results, - }) + ) } - async fn get_room_chunk( - &self, sender_user: &UserId, room_id: &RoomId, children: Vec>, - ) -> Result { - Ok(SpaceHierarchyRoomsChunk { + fn get_room_summary( + &self, current_room: &OwnedRoomId, children_state: Vec>, + identifier: Identifier<'_>, + ) -> Result { + 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 @@ -286,73 +534,30 @@ impl Service { .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| { + 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 - .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.")) - })?, + }, + )?, + world_readable: 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 so we must flatten - .flatten(), - join_rule: { - 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); - - if !self.handle_join_rule(&join_rule, sender_user, room_id)? { - 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")); - } - - self.translate_joinrule(&join_rule)? - }, + .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 so we must flatten + .flatten(), + join_rule, room_type: services() .rooms .state_accessor @@ -365,57 +570,631 @@ impl Service { }) .transpose()? .and_then(|e| e.room_type), - children_state: children.into_iter().map(|pdu| pdu.to_stripped_spacechild_state_event()).collect(), + children_state, + allowed_room_ids, }) } - fn translate_joinrule(&self, join_rule: &JoinRule) -> Result { - match join_rule { - JoinRule::Invite => Ok(SpaceRoomJoinRule::Invite), - JoinRule::Knock => Ok(SpaceRoomJoinRule::Knock), - JoinRule::Private => Ok(SpaceRoomJoinRule::Private), - JoinRule::Restricted(_) => Ok(SpaceRoomJoinRule::Restricted), - JoinRule::KnockRestricted(_) => Ok(SpaceRoomJoinRule::KnockRestricted), - JoinRule::Public => Ok(SpaceRoomJoinRule::Public), - _ => Err(Error::BadServerResponse("Unknown join rule")), + pub async fn get_client_hierarchy( + &self, sender_user: &UserId, room_id: &RoomId, limit: usize, skip: usize, max_depth: usize, + suggested_only: bool, + ) -> Result { + match self + .get_summary_and_children(&room_id.to_owned(), suggested_only, Identifier::UserId(sender_user)) + .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(*summary.clone(), suggested_only)); + results.push(summary_to_chunk(*summary.clone())); + + while let Some(current_room) = arena.first_untraversed() { + if limit > results.len() { + if let Some(SummaryAccessibility::Accessible(summary)) = self + .get_summary_and_children( + &arena.get(current_room).expect("We added this node, it must exist").room_id, + suggested_only, + Identifier::UserId(sender_user), + ) + .await? + { + let children = get_parent_children(*summary.clone(), 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")), } } +} - fn handle_simplified_join_rule( - &self, join_rule: &SpaceRoomJoinRule, sender_user: &UserId, room_id: &RoomId, - ) -> Result { - let allowed = match join_rule { - SpaceRoomJoinRule::Public => true, - SpaceRoomJoinRule::Knock => true, - SpaceRoomJoinRule::Invite => services().rooms.state_cache.is_joined(sender_user, room_id)?, - _ => false, - }; +/// Simply returns the stripped m.space.child events of a room +async fn get_stripped_space_child_events( + room_id: &RoomId, +) -> Result>>, 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; + } - Ok(allowed) - } + let pdu = services() + .rooms + .timeline + .get_pdu(&id)? + .ok_or_else(|| Error::bad_database("Event in space state not found"))?; - fn handle_join_rule(&self, join_rule: &JoinRule, sender_user: &UserId, room_id: &RoomId) -> Result { - if self.handle_simplified_join_rule(&self.translate_joinrule(join_rule)?, sender_user, room_id)? { - return Ok(true); + if serde_json::from_str::(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) + } +} - match join_rule { - JoinRule::Restricted(r) => { - for rule in &r.allow { - if let AllowRule::RoomMembership(rm) = rule { - if let Ok(true) = services().rooms.state_cache.is_joined(sender_user, &rm.room_id) { +/// 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, +) -> Result { + 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, recurse_num: usize, +) -> Result { + // Set limit at 10, as we cannot keep going up parents forever + 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); + } + }, + _ => (), + } // Takes care of joinrules + Ok(match join_rule { + SpaceRoomJoinRule::KnockRestricted | SpaceRoomJoinRule::Restricted => { + for room in allowed_room_ids { + if let Ok((join_rule, allowed_room_ids)) = get_join_rule(room) { + // Recursive, get rid of if possible + if let Ok(true) = is_accessable_child_recurse( + room, + &join_rule, + identifier, + &allowed_room_ids, + recurse_num + 1, + ) { return Ok(true); } } } - - Ok(false) + false }, - JoinRule::KnockRestricted(_) => { - // TODO: Check rules - Ok(false) - }, - _ => Ok(false), - } + SpaceRoomJoinRule::Public | SpaceRoomJoinRule::Knock => true, + SpaceRoomJoinRule::Invite | SpaceRoomJoinRule::Private => false, + // Custom join rule + _ => 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 { + 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.")) + }, + ) +} + +/// Checks if guests are able to view room content without joining +fn world_readable(room_id: &RoomId) -> Result { + 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.")) + }, + ) +} + +/// Returns the join rule for a given room +fn get_join_rule(current_room: &RoomId) -> Result<(SpaceRoomJoinRule, Vec), 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 { + 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.to_owned()); + } + } + } + room_ids +} + +/// Returns the children of a SpaceHierarchyParentSummary, making use of the +/// children_state field +fn get_parent_children(parent: SpaceHierarchyParentSummary, suggested_only: bool) -> Vec { + 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) + } + }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use ruma::{ + api::federation::space::SpaceHierarchyParentSummaryInit, events::room::join_rules::Restricted, owned_room_id, + }; + + 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"), + owned_room_id!("!subspace2:example.org"), + owned_room_id!("!foo:example.org"), + ], + ); + + let subspace1 = arena.first_untraversed().unwrap(); + let subspace2 = arena.first_untraversed().unwrap(); + + arena.push( + subspace1, + vec![owned_room_id!("!room1:example.org"), owned_room_id!("!room2:example.org")], + ); + + 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"), owned_room_id!("!room4:example.org")], + ); + + 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"), owned_room_id!("!room2:example.org")], + ); + + 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")]); + + 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"), + owned_room_id!("!subspace2:example.org"), + owned_room_id!("!foo:example.org"), + ], + ); + + let subspace1 = arena.first_untraversed().unwrap(); + arena.push( + subspace1, + vec![ + owned_room_id!("!room1:example.org"), + owned_room_id!("!room3:example.org"), + owned_room_id!("!room5:example.org"), + ], + ); + + 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"), owned_room_id!("!room2:example.org")], + ); + + 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(summary.clone(), false), + vec![ + owned_room_id!("!foo:example.org"), + owned_room_id!("!bar:example.org"), + owned_room_id!("!baz:example.org") + ] + ); + assert_eq!(get_parent_children(summary, true), vec![owned_room_id!("!bar:example.org")]); + } + + #[test] + fn allowed_room_ids_rom_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 = 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::from_str(token); + assert!(token.is_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"), + owned_room_id!("!room1:example.org"), + owned_room_id!("!subspace2:example.org"), + ], + ); + + let subspace1_node_id = arena.first_untraversed().unwrap(); + arena.push( + subspace1_node_id, + vec![owned_room_id!("!subspace2:example.org"), owned_room_id!("!room1:example.org")], + ); + + 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"), + owned_room_id!("!subspace2:example.org"), + owned_room_id!("!room1:example.org"), + ], + ); + + 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()); } } diff --git a/src/service/rooms/state/mod.rs b/src/service/rooms/state/mod.rs index 3273d9c2..db2c1921 100644 --- a/src/service/rooms/state/mod.rs +++ b/src/service/rooms/state/mod.rs @@ -74,7 +74,7 @@ impl Service { .await?; }, TimelineEventType::SpaceChild => { - services().rooms.spaces.roomid_spacechunk_cache.lock().await.remove(&pdu.room_id); + services().rooms.spaces.roomid_spacehierarchy_cache.lock().await.remove(&pdu.room_id); }, _ => continue, } diff --git a/src/service/rooms/timeline/mod.rs b/src/service/rooms/timeline/mod.rs index 92f0a09d..0d9ed3be 100644 --- a/src/service/rooms/timeline/mod.rs +++ b/src/service/rooms/timeline/mod.rs @@ -418,7 +418,7 @@ impl Service { }, TimelineEventType::SpaceChild => { if let Some(_state_key) = &pdu.state_key { - services().rooms.spaces.roomid_spacechunk_cache.lock().await.remove(&pdu.room_id); + services().rooms.spaces.roomid_spacehierarchy_cache.lock().await.remove(&pdu.room_id); } }, TimelineEventType::RoomMember => {