feat(spaces): add default roles init and startup cache rebuild

Add ensure_default_roles() to check if a Space has m.space.roles state
event and create default admin/mod roles if missing. Add worker() to
rebuild the space roles cache on startup by iterating all rooms and
populating cache for spaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ember33 2026-03-18 09:52:10 +01:00
parent e3a0ab2214
commit c8f39ca6ff

View file

@ -12,7 +12,12 @@ use std::{
};
use async_trait::async_trait;
use conduwuit::{Event, Result, Server, debug_warn, implement, matrix::pdu::PduBuilder, warn};
use conduwuit::{
Event, Result, Server, debug, debug_warn, implement, info,
matrix::pdu::PduBuilder,
warn,
};
use serde_json::value::to_raw_value;
use conduwuit_core::{
matrix::space_roles::{
RoleDefinition, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent,
@ -25,7 +30,7 @@ use conduwuit_core::{
};
use futures::{StreamExt, TryFutureExt};
use ruma::{
Int, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId,
Int, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, UserId, room::RoomType,
events::{
StateEventType,
room::{
@ -54,6 +59,7 @@ pub struct Service {
struct Services {
globals: Dep<globals::Service>,
metadata: Dep<rooms::metadata::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
state_cache: Dep<rooms::state_cache::Service>,
state: Dep<rooms::state::Service>,
@ -68,6 +74,7 @@ impl crate::Service for Service {
Ok(Arc::new(Self {
services: Services {
globals: args.depend::<globals::Service>("globals"),
metadata: args.depend::<rooms::metadata::Service>("rooms::metadata"),
state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
@ -104,6 +111,37 @@ impl crate::Service for Service {
self.room_to_space.write().await.clear();
}
async fn worker(self: Arc<Self>) -> Result<()> {
if !self.is_enabled() {
return Ok(());
}
info!("Rebuilding space roles cache from all known rooms...");
let mut space_count: usize = 0;
let room_ids: Vec<OwnedRoomId> = self
.services
.metadata
.iter_ids()
.map(ToOwned::to_owned)
.collect()
.await;
for room_id in &room_ids {
match self.services.state_accessor.get_room_type(room_id).await {
| Ok(RoomType::Space) => {
debug!("Populating space roles cache for {room_id}");
self.populate_space(room_id).await;
space_count += 1;
},
| _ => continue,
}
}
info!("Space roles cache rebuilt for {space_count} spaces");
Ok(())
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
@ -111,6 +149,69 @@ impl crate::Service for Service {
#[implement(Service)]
pub fn is_enabled(&self) -> bool { self.server.config.space_permission_cascading }
/// Ensure a Space has the default admin/mod roles defined.
///
/// Checks whether an `m.space.roles` state event exists in the given space.
/// If not, creates default roles (admin at PL 100, mod at PL 50) and sends
/// the state event as the server user.
#[implement(Service)]
pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
if !self.is_enabled() {
return Ok(());
}
// Check if m.space.roles already exists
let roles_event_type = StateEventType::from("m.space.roles".to_owned());
if self
.services
.state_accessor
.room_state_get_content::<SpaceRolesEventContent>(space_id, &roles_event_type, "")
.await
.is_ok()
{
return Ok(());
}
// Create default roles
let mut roles = BTreeMap::new();
roles.insert(
"admin".to_owned(),
RoleDefinition {
description: "Space administrator".to_owned(),
power_level: Some(100),
},
);
roles.insert(
"mod".to_owned(),
RoleDefinition {
description: "Space moderator".to_owned(),
power_level: Some(50),
},
);
let content = SpaceRolesEventContent { roles };
let sender = self.services.globals.server_user.as_ref();
let state_lock = self.services.state.mutex.lock(space_id).await;
let pdu = PduBuilder {
event_type: ruma::events::TimelineEventType::from("m.space.roles".to_owned()),
content: to_raw_value(&content)
.expect("Failed to serialize SpaceRolesEventContent"),
state_key: Some(String::new().into()),
..PduBuilder::default()
};
self.services
.timeline
.build_and_append_pdu(pdu, sender, Some(space_id), &state_lock)
.await?;
debug!("Sent default m.space.roles event for {space_id}");
Ok(())
}
/// 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