diff --git a/src/service/rooms/roles/mod.rs b/src/service/rooms/roles/mod.rs index 0d2ccb8f..155f9909 100644 --- a/src/service/rooms/roles/mod.rs +++ b/src/service/rooms/roles/mod.rs @@ -5,16 +5,32 @@ use std::{ }; use async_trait::async_trait; -use conduwuit::Result; -use conduwuit_core::matrix::space_roles::RoleDefinition; -use ruma::{OwnedRoomId, OwnedUserId}; +use conduwuit::{Event, Result, Server, implement}; +use conduwuit_core::{ + matrix::space_roles::{ + RoleDefinition, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, + SpaceRolesEventContent, + }, + utils::{ + future::TryExtExt, + stream::{BroadbandExt, ReadyExt}, + }, +}; +use futures::{StreamExt, TryFutureExt}; +use ruma::{ + OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, + events::{ + StateEventType, + space::child::SpaceChildEventContent, + }, +}; use tokio::sync::RwLock; use crate::{Dep, rooms}; pub struct Service { - #[allow(dead_code)] services: Services, + server: Arc, /// Space ID -> role name -> role definition pub roles: RwLock>>, /// Space ID -> user ID -> assigned role names @@ -25,11 +41,12 @@ pub struct Service { pub room_to_space: RwLock>, } -#[allow(dead_code)] struct Services { state_accessor: Dep, + #[allow(dead_code)] state_cache: Dep, state: Dep, + #[allow(dead_code)] spaces: Dep, timeline: Dep, } @@ -46,6 +63,7 @@ impl crate::Service for Service { spaces: args.depend::("rooms::spaces"), timeline: args.depend::("rooms::timeline"), }, + server: args.server.clone(), roles: RwLock::new(HashMap::new()), user_roles: RwLock::new(HashMap::new()), room_requirements: RwLock::new(HashMap::new()), @@ -76,3 +94,187 @@ impl crate::Service for Service { fn name(&self) -> &str { crate::service::make_name(std::module_path!()) } } + +/// Check whether space permission cascading is enabled in the server config. +#[implement(Service)] +pub fn is_enabled(&self) -> bool { self.server.config.space_permission_cascading } + +/// Populate the in-memory caches from state events for a single Space room. +/// +/// Reads `m.space.roles`, `m.space.role.member`, `m.space.role.room`, and +/// `m.space.child` state events and indexes them for fast lookup. +#[implement(Service)] +pub async fn populate_space(&self, space_id: &RoomId) { + // 1. Read m.space.roles (state key: "") + let roles_event_type = StateEventType::from("m.space.roles".to_owned()); + if let Ok(content) = self + .services + .state_accessor + .room_state_get_content::(space_id, &roles_event_type, "") + .await + { + self.roles + .write() + .await + .insert(space_id.to_owned(), content.roles); + } + + // 2. Read all m.space.role.member state events (state key: user ID) + let member_event_type = StateEventType::from("m.space.role.member".to_owned()); + if let Ok(shortstatehash) = self + .services + .state + .get_room_shortstatehash(space_id) + .await + { + let mut user_roles_map: HashMap> = HashMap::new(); + + self.services + .state_accessor + .state_keys_with_ids(shortstatehash, &member_event_type) + .boxed() + .broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move { + self.services + .timeline + .get_pdu(&event_id) + .map_ok(move |pdu| (state_key, pdu)) + .ok() + .await + }) + .ready_filter_map(|(state_key, pdu)| { + let content = pdu.get_content::().ok()?; + let user_id = UserId::parse(&*state_key).ok()?.to_owned(); + Some((user_id, content.roles)) + }) + .for_each(|(user_id, roles)| { + user_roles_map.insert(user_id, roles.into_iter().collect()); + async {} + }) + .await; + + self.user_roles + .write() + .await + .insert(space_id.to_owned(), user_roles_map); + + // 3. Read all m.space.role.room state events (state key: room ID) + let room_event_type = StateEventType::from("m.space.role.room".to_owned()); + let mut room_reqs_map: HashMap> = HashMap::new(); + + self.services + .state_accessor + .state_keys_with_ids(shortstatehash, &room_event_type) + .boxed() + .broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move { + self.services + .timeline + .get_pdu(&event_id) + .map_ok(move |pdu| (state_key, pdu)) + .ok() + .await + }) + .ready_filter_map(|(state_key, pdu)| { + let content = pdu.get_content::().ok()?; + let room_id = RoomId::parse(&*state_key).ok()?.to_owned(); + Some((room_id, content.required_roles)) + }) + .for_each(|(room_id, required_roles)| { + room_reqs_map.insert(room_id, required_roles.into_iter().collect()); + async {} + }) + .await; + + self.room_requirements + .write() + .await + .insert(space_id.to_owned(), room_reqs_map); + + // 4. Read all m.space.child state events → build room_to_space reverse index + self.services + .state_accessor + .state_keys_with_ids(shortstatehash, &StateEventType::SpaceChild) + .boxed() + .broad_filter_map(|(state_key, event_id): (_, OwnedEventId)| async move { + self.services + .timeline + .get_pdu(&event_id) + .map_ok(move |pdu| (state_key, pdu)) + .ok() + .await + }) + .ready_filter_map(|(state_key, pdu)| { + // Only index children that have a valid via list + if let Ok(content) = pdu.get_content::() { + if content.via.is_empty() { + return None; + } + } else { + return None; + } + let child_room_id = RoomId::parse(&*state_key).ok()?.to_owned(); + Some(child_room_id) + }) + .for_each(|child_room_id| { + let space_owned = space_id.to_owned(); + async move { + self.room_to_space + .write() + .await + .insert(child_room_id, space_owned); + } + }) + .await; + } +} + +/// Get a user's effective power level from Space roles. +/// Returns None if user has no roles with power levels. +#[implement(Service)] +pub async fn get_user_power_level( + &self, + space_id: &RoomId, + user_id: &UserId, +) -> Option { + let roles_map = self.roles.read().await; + let user_roles_map = self.user_roles.read().await; + let role_defs = roles_map.get(space_id)?; + let user_assigned = user_roles_map.get(space_id)?.get(user_id)?; + user_assigned + .iter() + .filter_map(|role_name| role_defs.get(role_name)?.power_level) + .max() +} + +/// Check if a user has all required roles for a room. +#[implement(Service)] +pub async fn user_qualifies_for_room( + &self, + space_id: &RoomId, + room_id: &RoomId, + user_id: &UserId, +) -> bool { + let reqs = self.room_requirements.read().await; + let Some(space_reqs) = reqs.get(space_id) else { + return true; + }; + let Some(required) = space_reqs.get(room_id) else { + return true; + }; + if required.is_empty() { + return true; + } + let user_map = self.user_roles.read().await; + let Some(space_users) = user_map.get(space_id) else { + return false; + }; + let Some(user_assigned) = space_users.get(user_id) else { + return false; + }; + required.iter().all(|r| user_assigned.contains(r)) +} + +/// Get the parent Space of a child room, if any. +#[implement(Service)] +pub async fn get_parent_space(&self, room_id: &RoomId) -> Option { + self.room_to_space.read().await.get(room_id).cloned() +}