diff options
Diffstat (limited to 'src/service/rooms/spaces/mod.rs')
-rw-r--r-- | src/service/rooms/spaces/mod.rs | 505 |
1 files changed, 505 insertions, 0 deletions
diff --git a/src/service/rooms/spaces/mod.rs b/src/service/rooms/spaces/mod.rs new file mode 100644 index 0000000..53232f4 --- /dev/null +++ b/src/service/rooms/spaces/mod.rs @@ -0,0 +1,505 @@ +use std::sync::{Arc, Mutex}; + +use lru_cache::LruCache; +use ruma::{ + api::{ + client::{ + error::ErrorKind, + space::{get_hierarchy, SpaceHierarchyRoomsChunk}, + }, + federation, + }, + events::{ + room::{ + avatar::RoomAvatarEventContent, + canonical_alias::RoomCanonicalAliasEventContent, + create::RoomCreateEventContent, + guest_access::{GuestAccess, RoomGuestAccessEventContent}, + history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent}, + join_rules::{self, AllowRule, JoinRule, RoomJoinRulesEventContent}, + topic::RoomTopicEventContent, + }, + space::child::SpaceChildEventContent, + StateEventType, + }, + space::SpaceRoomJoinRule, + OwnedRoomId, RoomId, UserId, +}; + +use tracing::{debug, error, warn}; + +use crate::{services, Error, PduEvent, Result}; + +pub enum CachedJoinRule { + //Simplified(SpaceRoomJoinRule), + Full(JoinRule), +} + +pub struct CachedSpaceChunk { + chunk: SpaceHierarchyRoomsChunk, + children: Vec<OwnedRoomId>, + join_rule: CachedJoinRule, +} + +pub struct Service { + pub roomid_spacechunk_cache: Mutex<LruCache<OwnedRoomId, Option<CachedSpaceChunk>>>, +} + +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<get_hierarchy::v1::Response> { + let mut left_to_skip = skip; + + let mut rooms_in_path = Vec::new(); + let mut stack = vec![vec![room_id.to_owned()]]; + let mut results = Vec::new(); + + while let Some(current_room) = { + while stack.last().map_or(false, |s| s.is_empty()) { + stack.pop(); + } + if !stack.is_empty() { + stack.last_mut().and_then(|s| s.pop()) + } else { + None + } + } { + rooms_in_path.push(current_room.clone()); + if results.len() >= limit { + break; + } + + if let Some(cached) = self + .roomid_spacechunk_cache + .lock() + .unwrap() + .get_mut(¤t_room.to_owned()) + .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::<SpaceChildEventContent>(pdu.content.get()) + .ok() + .and_then(|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); + 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().unwrap().insert( + current_room.clone(), + Some(CachedSpaceChunk { + chunk, + children: children_ids.clone(), + join_rule: CachedJoinRule::Full(join_rule), + }), + ); + } + + if rooms_in_path.len() < max_depth { + stack.push(children_ids); + } + } else { + let server = current_room.server_name(); + if server == services().globals.server_name() { + continue; + } + if !results.is_empty() { + // Early return so the client can see some data already + break; + } + warn!("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 + { + warn!("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::<Vec<_>>(); + + 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(|room| AllowRule::room_membership(room)) + .collect(), + }) + } + SpaceRoomJoinRule::KnockRestricted => { + JoinRule::KnockRestricted(join_rules::Restricted { + allow: response + .room + .allowed_room_ids + .into_iter() + .map(|room| AllowRule::room_membership(room)) + .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()); + } + } + + self.roomid_spacechunk_cache.lock().unwrap().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, + }, + ); + } + */ + } else { + self.roomid_spacechunk_cache + .lock() + .unwrap() + .insert(current_room.clone(), None); + } + } + } + + Ok(get_hierarchy::v1::Response { + next_batch: if results.is_empty() { + None + } else { + Some((skip + results.len()).to_string()) + }, + rooms: results, + }) + } + + fn get_room_chunk( + &self, + sender_user: &UserId, + room_id: &RoomId, + children: Vec<Arc<PduEvent>>, + ) -> Result<SpaceHierarchyRoomsChunk> { + Ok(SpaceHierarchyRoomsChunk { + 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.") + }) + })?, + 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, "")? + .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: { + 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)? + }, + 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: children + .into_iter() + .map(|pdu| pdu.to_stripped_spacechild_state_event()) + .collect(), + }) + } + + fn translate_joinrule(&self, join_rule: &JoinRule) -> Result<SpaceRoomJoinRule> { + 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")), + } + } + + fn handle_simplified_join_rule( + &self, + join_rule: &SpaceRoomJoinRule, + sender_user: &UserId, + room_id: &RoomId, + ) -> Result<bool> { + let allowed = match join_rule { + SpaceRoomJoinRule::Public => true, + SpaceRoomJoinRule::Knock => true, + SpaceRoomJoinRule::Invite => services() + .rooms + .state_cache + .is_joined(sender_user, &room_id)?, + _ => false, + }; + + Ok(allowed) + } + + fn handle_join_rule( + &self, + join_rule: &JoinRule, + sender_user: &UserId, + room_id: &RoomId, + ) -> Result<bool> { + if self.handle_simplified_join_rule( + &self.translate_joinrule(join_rule)?, + sender_user, + room_id, + )? { + return Ok(true); + } + + match join_rule { + JoinRule::Restricted(r) => { + for rule in &r.allow { + match rule { + join_rules::AllowRule::RoomMembership(rm) => { + if let Ok(true) = services() + .rooms + .state_cache + .is_joined(sender_user, &rm.room_id) + { + return Ok(true); + } + } + _ => {} + } + } + + Ok(false) + } + JoinRule::KnockRestricted(_) => { + // TODO: Check rules + Ok(false) + } + _ => Ok(false), + } + } +} |