From c8f39ca6ffcb69a40d26b5c043b64f9f05711c80 Mon Sep 17 00:00:00 2001 From: ember33 Date: Wed, 18 Mar 2026 09:52:10 +0100 Subject: [PATCH] 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) --- src/service/rooms/roles/mod.rs | 105 ++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 2 deletions(-) diff --git a/src/service/rooms/roles/mod.rs b/src/service/rooms/roles/mod.rs index 9321ba67..ae8d40f1 100644 --- a/src/service/rooms/roles/mod.rs +++ b/src/service/rooms/roles/mod.rs @@ -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, + metadata: Dep, state_accessor: Dep, state_cache: Dep, state: Dep, @@ -68,6 +74,7 @@ impl crate::Service for Service { Ok(Arc::new(Self { services: Services { globals: args.depend::("globals"), + metadata: args.depend::("rooms::metadata"), state_accessor: args .depend::("rooms::state_accessor"), state_cache: args.depend::("rooms::state_cache"), @@ -104,6 +111,37 @@ impl crate::Service for Service { self.room_to_space.write().await.clear(); } + async fn worker(self: Arc) -> 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 = 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::(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