feat(spaces): add cache population and lookup methods for space roles
Adds is_enabled(), populate_space(), get_user_power_level(), user_qualifies_for_room(), and get_parent_space() methods. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aae610b3d2
commit
83eea18f3e
1 changed files with 207 additions and 5 deletions
|
|
@ -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<Server>,
|
||||
/// Space ID -> role name -> role definition
|
||||
pub roles: RwLock<HashMap<OwnedRoomId, BTreeMap<String, RoleDefinition>>>,
|
||||
/// Space ID -> user ID -> assigned role names
|
||||
|
|
@ -25,11 +41,12 @@ pub struct Service {
|
|||
pub room_to_space: RwLock<HashMap<OwnedRoomId, OwnedRoomId>>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Services {
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
#[allow(dead_code)]
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
state: Dep<rooms::state::Service>,
|
||||
#[allow(dead_code)]
|
||||
spaces: Dep<rooms::spaces::Service>,
|
||||
timeline: Dep<rooms::timeline::Service>,
|
||||
}
|
||||
|
|
@ -46,6 +63,7 @@ impl crate::Service for Service {
|
|||
spaces: args.depend::<rooms::spaces::Service>("rooms::spaces"),
|
||||
timeline: args.depend::<rooms::timeline::Service>("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::<SpaceRolesEventContent>(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<OwnedUserId, HashSet<String>> = 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::<SpaceRoleMemberEventContent>().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<OwnedRoomId, HashSet<String>> = 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::<SpaceRoleRoomEventContent>().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::<SpaceChildEventContent>() {
|
||||
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<i64> {
|
||||
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<OwnedRoomId> {
|
||||
self.room_to_space.read().await.get(room_id).cloned()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue