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, ) -> Result { // 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::() .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 = 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>, ) -> Result { 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::::new(&hsce).ok()) .collect::>(), room_id, }) }