summaryrefslogtreecommitdiff
path: root/src/service/rooms/spaces/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/service/rooms/spaces/mod.rs')
-rw-r--r--src/service/rooms/spaces/mod.rs505
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(&current_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, &current_room)?
+ //}
+ CachedJoinRule::Full(f) => {
+ self.handle_join_rule(f, sender_user, &current_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(&current_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, &current_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(&current_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, &current_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),
+ }
+ }
+}