feat(spaces): add per-Space cascading toggle with server-wide default
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 4s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 14s
Some checks failed
Documentation / Build and Deploy Documentation (pull_request) Has been skipped
Checks / Prek / Pre-commit & Formatting (pull_request) Failing after 4s
Checks / Prek / Clippy and Cargo Tests (pull_request) Failing after 5s
Update flake hashes / update-flake-hashes (pull_request) Failing after 14s
Add com.continuwuity.space.cascading state event for per-Space override of the server-wide space_permission_cascading config. Add enable/disable/ status admin commands. Strip superfluous comments throughout.
This commit is contained in:
parent
53d4fb892c
commit
5b56a8b6ed
8 changed files with 251 additions and 232 deletions
|
|
@ -470,9 +470,10 @@
|
|||
#
|
||||
#suspend_on_register = false
|
||||
|
||||
# Enable space permission cascading (power levels and role-based access).
|
||||
# When enabled, power levels cascade from Spaces to child rooms and rooms
|
||||
# can require roles for access. Applies to all Spaces on this server.
|
||||
# Server-wide default for space permission cascading (power levels and
|
||||
# role-based access). Individual Spaces can override this via the
|
||||
# `com.continuwuity.space.cascading` state event or the admin command
|
||||
# `!admin space roles enable/disable <space>`.
|
||||
#
|
||||
#space_permission_cascading = false
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ use std::fmt::Write;
|
|||
use clap::Subcommand;
|
||||
use conduwuit::{Err, Event, Result, matrix::pdu::PduBuilder};
|
||||
use conduwuit_core::matrix::space_roles::{
|
||||
RoleDefinition, SPACE_ROLE_MEMBER_EVENT_TYPE, SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
SPACE_ROLES_EVENT_TYPE, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent,
|
||||
SpaceRolesEventContent,
|
||||
RoleDefinition, SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE, SpaceCascadingEventContent,
|
||||
SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, SpaceRolesEventContent,
|
||||
};
|
||||
use futures::StreamExt;
|
||||
use ruma::{OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, events::StateEventType};
|
||||
|
|
@ -13,23 +13,24 @@ use serde_json::value::to_raw_value;
|
|||
|
||||
use crate::{admin_command, admin_command_dispatch};
|
||||
|
||||
macro_rules! require_enabled {
|
||||
($self:expr) => {
|
||||
if !$self.services.rooms.roles.is_enabled() {
|
||||
macro_rules! resolve_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
let space_id = $self.services.rooms.alias.resolve(&$space).await?;
|
||||
if !$self
|
||||
.services
|
||||
.rooms
|
||||
.roles
|
||||
.is_enabled_for_space(&space_id)
|
||||
.await
|
||||
{
|
||||
return $self
|
||||
.write_str(
|
||||
"Space permission cascading is disabled. Enable it with \
|
||||
`space_permission_cascading = true` in your config.",
|
||||
"Space permission cascading is disabled for this Space. Enable it \
|
||||
server-wide with `space_permission_cascading = true` in your config, or \
|
||||
per-Space with `!admin space roles enable <space>`.",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! resolve_space {
|
||||
($self:expr, $space:expr) => {{
|
||||
require_enabled!($self);
|
||||
let space_id = $self.services.rooms.alias.resolve(&$space).await?;
|
||||
if !matches!(
|
||||
$self
|
||||
.services
|
||||
|
|
@ -115,6 +116,21 @@ pub enum SpaceRolesCommand {
|
|||
space: OwnedRoomOrAliasId,
|
||||
room_id: OwnedRoomId,
|
||||
},
|
||||
/// Enable space permission cascading for a specific space (overrides
|
||||
/// server config)
|
||||
Enable {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Disable space permission cascading for a specific space (overrides
|
||||
/// server config)
|
||||
Disable {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
/// Show whether cascading is enabled for a space and the source (server
|
||||
/// default or per-space override)
|
||||
Status {
|
||||
space: OwnedRoomOrAliasId,
|
||||
},
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
|
|
@ -314,7 +330,6 @@ async fn assign(
|
|||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
// Read current role definitions to validate the role name
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
|
|
@ -414,7 +429,6 @@ async fn require(
|
|||
) -> Result {
|
||||
let space_id = resolve_space!(self, space);
|
||||
|
||||
// Read current role definitions to validate the role name
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
let role_defs: SpaceRolesEventContent = self
|
||||
.services
|
||||
|
|
@ -566,3 +580,116 @@ async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result
|
|||
.await,
|
||||
}
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn enable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: true };
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_CASCADING_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.roles
|
||||
.ensure_default_roles(&space_id)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Space permission cascading enabled for {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn disable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
|
||||
let content = SpaceCascadingEventContent { enabled: false };
|
||||
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
custom_state_pdu!(SPACE_CASCADING_EVENT_TYPE, "", &content),
|
||||
server_user,
|
||||
Some(&space_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.write_str(&format!("Space permission cascading disabled for {space_id}."))
|
||||
.await
|
||||
}
|
||||
|
||||
#[admin_command]
|
||||
async fn status(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||
let space_id = self.services.rooms.alias.resolve(&space).await?;
|
||||
if !matches!(
|
||||
self.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_room_type(&space_id)
|
||||
.await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
return Err!("The specified room is not a Space.");
|
||||
}
|
||||
|
||||
let global_default = self.services.rooms.roles.is_enabled();
|
||||
let cascading_event_type = StateEventType::from(SPACE_CASCADING_EVENT_TYPE.to_owned());
|
||||
let per_space_override: Option<bool> = self
|
||||
.services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.room_state_get_content::<SpaceCascadingEventContent>(
|
||||
&space_id,
|
||||
&cascading_event_type,
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(|c| c.enabled);
|
||||
|
||||
let effective = per_space_override.unwrap_or(global_default);
|
||||
let source = match per_space_override {
|
||||
| Some(v) => format!("per-Space override (enabled: {v})"),
|
||||
| None => format!("server default (space_permission_cascading: {global_default})"),
|
||||
};
|
||||
|
||||
self.write_str(&format!(
|
||||
"Cascading status for {space_id}:\n- Effective: **{effective}**\n- Source: {source}"
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
|
|
|||
|
|
@ -347,9 +347,7 @@ pub async fn join_room_by_id_helper(
|
|||
}
|
||||
}
|
||||
|
||||
// Space permission cascading: check if user has required roles
|
||||
// User must qualify in at least one parent space (if any exist)
|
||||
if services.rooms.roles.is_enabled() {
|
||||
{
|
||||
let parent_spaces = services.rooms.roles.get_parent_spaces(room_id).await;
|
||||
if !parent_spaces.is_empty() {
|
||||
let mut qualifies_in_any = false;
|
||||
|
|
|
|||
|
|
@ -603,9 +603,10 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub suspend_on_register: bool,
|
||||
|
||||
/// Enable space permission cascading (power levels and role-based access).
|
||||
/// When enabled, power levels cascade from Spaces to child rooms and rooms
|
||||
/// can require roles for access. Applies to all Spaces on this server.
|
||||
/// Server-wide default for space permission cascading (power levels and
|
||||
/// role-based access). Individual Spaces can override this via the
|
||||
/// `com.continuwuity.space.cascading` state event or the admin command
|
||||
/// `!admin space roles enable/disable <space>`.
|
||||
///
|
||||
/// default: false
|
||||
#[serde(default)]
|
||||
|
|
|
|||
|
|
@ -1,56 +1,39 @@
|
|||
//! Custom state event content types for space permission cascading.
|
||||
//!
|
||||
//! These events live in Space rooms and define roles, user-role assignments,
|
||||
//! and room-role requirements.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Custom event type for space role definitions.
|
||||
pub const SPACE_ROLES_EVENT_TYPE: &str = "com.continuwuity.space.roles";
|
||||
|
||||
/// Custom event type for per-user role assignments within a space.
|
||||
pub const SPACE_ROLE_MEMBER_EVENT_TYPE: &str = "com.continuwuity.space.role.member";
|
||||
|
||||
/// Custom event type for per-room role requirements within a space.
|
||||
pub const SPACE_ROLE_ROOM_EVENT_TYPE: &str = "com.continuwuity.space.role.room";
|
||||
pub const SPACE_CASCADING_EVENT_TYPE: &str = "com.continuwuity.space.cascading";
|
||||
|
||||
/// Content for `com.continuwuity.space.roles` (state key: "")
|
||||
///
|
||||
/// Defines available roles for a Space.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRolesEventContent {
|
||||
pub roles: BTreeMap<String, RoleDefinition>,
|
||||
}
|
||||
|
||||
/// A single role definition within a Space.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RoleDefinition {
|
||||
pub description: String,
|
||||
|
||||
/// If present, users with this role receive this power level in child
|
||||
/// rooms.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub power_level: Option<i64>,
|
||||
}
|
||||
|
||||
/// Content for `com.continuwuity.space.role.member` (state key: user ID)
|
||||
///
|
||||
/// Assigns roles to a user within a Space.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRoleMemberEventContent {
|
||||
pub roles: Vec<String>,
|
||||
}
|
||||
|
||||
/// Content for `com.continuwuity.space.role.room` (state key: room ID)
|
||||
///
|
||||
/// Declares which roles a child room requires for access.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceRoleRoomEventContent {
|
||||
pub required_roles: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SpaceCascadingEventContent {
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ use conduwuit::{
|
|||
};
|
||||
use conduwuit_core::{
|
||||
matrix::space_roles::{
|
||||
RoleDefinition, SPACE_ROLE_MEMBER_EVENT_TYPE, SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
SPACE_ROLES_EVENT_TYPE, SpaceRoleMemberEventContent, SpaceRoleRoomEventContent,
|
||||
SpaceRolesEventContent,
|
||||
RoleDefinition, SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||
SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE, SpaceCascadingEventContent,
|
||||
SpaceRoleMemberEventContent, SpaceRoleRoomEventContent, SpaceRolesEventContent,
|
||||
},
|
||||
utils::{
|
||||
future::TryExtExt,
|
||||
|
|
@ -129,10 +129,6 @@ impl crate::Service for Service {
|
|||
}
|
||||
|
||||
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;
|
||||
|
|
@ -147,6 +143,11 @@ impl crate::Service for Service {
|
|||
for room_id in &room_ids {
|
||||
match self.services.state_accessor.get_room_type(room_id).await {
|
||||
| Ok(RoomType::Space) => {
|
||||
// Check per-Space override — skip spaces where cascading is
|
||||
// disabled
|
||||
if !self.is_enabled_for_space(room_id).await {
|
||||
continue;
|
||||
}
|
||||
debug!(room_id = %room_id, "Populating space roles cache");
|
||||
self.populate_space(room_id).await;
|
||||
space_count = space_count.saturating_add(1);
|
||||
|
|
@ -162,22 +163,30 @@ 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 }
|
||||
|
||||
/// Ensure a Space has the default admin/mod roles defined.
|
||||
///
|
||||
/// Checks whether a `com.continuwuity.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 is_enabled_for_space(&self, space_id: &RoomId) -> bool {
|
||||
let cascading_event_type = StateEventType::from(SPACE_CASCADING_EVENT_TYPE.to_owned());
|
||||
if let Ok(content) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.room_state_get_content::<SpaceCascadingEventContent>(space_id, &cascading_event_type, "")
|
||||
.await
|
||||
{
|
||||
return content.enabled;
|
||||
}
|
||||
|
||||
self.server.config.space_permission_cascading
|
||||
}
|
||||
|
||||
#[implement(Service)]
|
||||
pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
|
||||
if !self.is_enabled() {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if com.continuwuity.space.roles already exists
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
if self
|
||||
.services
|
||||
|
|
@ -189,7 +198,6 @@ pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// Create default roles
|
||||
let mut roles = BTreeMap::new();
|
||||
roles.insert("admin".to_owned(), RoleDefinition {
|
||||
description: "Space administrator".to_owned(),
|
||||
|
|
@ -226,19 +234,12 @@ pub async fn ensure_default_roles(&self, space_id: &RoomId) -> Result {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the in-memory caches from state events for a single Space room.
|
||||
///
|
||||
/// Reads `com.continuwuity.space.roles`, `com.continuwuity.space.role.member`,
|
||||
/// `com.continuwuity.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) {
|
||||
if !self.is_enabled() {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check cache capacity — if over limit, clear and let spaces repopulate on
|
||||
// demand
|
||||
if self.roles.read().await.len()
|
||||
>= usize::try_from(self.server.config.space_roles_cache_capacity).unwrap_or(usize::MAX)
|
||||
{
|
||||
|
|
@ -250,7 +251,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
debug_warn!("Space roles cache exceeded capacity, cleared");
|
||||
}
|
||||
|
||||
// 1. Read com.continuwuity.space.roles (state key: "")
|
||||
let roles_event_type = StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned());
|
||||
if let Ok(content) = self
|
||||
.services
|
||||
|
|
@ -264,8 +264,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
.insert(space_id.to_owned(), content.roles);
|
||||
}
|
||||
|
||||
// 2. Read all com.continuwuity.space.role.member state events (state key: user
|
||||
// ID)
|
||||
let member_event_type = StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned());
|
||||
let shortstatehash = match self.services.state.get_room_shortstatehash(space_id).await {
|
||||
| Ok(hash) => hash,
|
||||
|
|
@ -305,8 +303,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
.await
|
||||
.insert(space_id.to_owned(), user_roles_map);
|
||||
|
||||
// 3. Read all com.continuwuity.space.role.room state events (state key: room
|
||||
// ID)
|
||||
let room_event_type = StateEventType::from(SPACE_ROLE_ROOM_EVENT_TYPE.to_owned());
|
||||
let mut room_reqs_map: HashMap<OwnedRoomId, HashSet<String>> = HashMap::new();
|
||||
|
||||
|
|
@ -338,7 +334,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
.await
|
||||
.insert(space_id.to_owned(), room_reqs_map);
|
||||
|
||||
// 4. Read all m.space.child state events → build room_to_space reverse index
|
||||
let mut child_rooms: Vec<OwnedRoomId> = Vec::new();
|
||||
|
||||
self.services
|
||||
|
|
@ -370,16 +365,12 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
})
|
||||
.await;
|
||||
|
||||
// Lock ordering: room_to_space before space_to_rooms.
|
||||
// This order must be consistent to avoid deadlocks.
|
||||
{
|
||||
let mut room_to_space = self.room_to_space.write().await;
|
||||
// Remove this space from all existing entries
|
||||
room_to_space.retain(|_, parents| {
|
||||
parents.remove(space_id);
|
||||
!parents.is_empty()
|
||||
});
|
||||
// Insert fresh children
|
||||
for child_room_id in &child_rooms {
|
||||
room_to_space
|
||||
.entry(child_room_id.clone())
|
||||
|
|
@ -388,7 +379,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
}
|
||||
}
|
||||
|
||||
// Update forward index (after room_to_space to maintain lock ordering)
|
||||
{
|
||||
let mut space_to_rooms = self.space_to_rooms.write().await;
|
||||
space_to_rooms.insert(space_id.to_owned(), child_rooms.into_iter().collect())
|
||||
|
|
@ -396,7 +386,6 @@ pub async fn populate_space(&self, space_id: &RoomId) {
|
|||
}
|
||||
}
|
||||
|
||||
/// Compute the maximum power level from a user's assigned roles.
|
||||
#[must_use]
|
||||
pub fn compute_user_power_level<S: ::std::hash::BuildHasher>(
|
||||
role_defs: &BTreeMap<String, RoleDefinition>,
|
||||
|
|
@ -408,7 +397,6 @@ pub fn compute_user_power_level<S: ::std::hash::BuildHasher>(
|
|||
.max()
|
||||
}
|
||||
|
||||
/// Check if a set of assigned roles satisfies all requirements.
|
||||
#[must_use]
|
||||
pub fn roles_satisfy_requirements<S: ::std::hash::BuildHasher>(
|
||||
required: &HashSet<String, S>,
|
||||
|
|
@ -417,8 +405,6 @@ pub fn roles_satisfy_requirements<S: ::std::hash::BuildHasher>(
|
|||
required.iter().all(|r| assigned.contains(r))
|
||||
}
|
||||
|
||||
/// 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 role_defs = { self.roles.read().await.get(space_id).cloned()? };
|
||||
|
|
@ -433,7 +419,6 @@ pub async fn get_user_power_level(&self, space_id: &RoomId, user_id: &UserId) ->
|
|||
compute_user_power_level(&role_defs, &user_assigned)
|
||||
}
|
||||
|
||||
/// Check if a user has all required roles for a room.
|
||||
#[implement(Service)]
|
||||
pub async fn user_qualifies_for_room(
|
||||
&self,
|
||||
|
|
@ -467,25 +452,25 @@ pub async fn user_qualifies_for_room(
|
|||
roles_satisfy_requirements(&required, &user_assigned)
|
||||
}
|
||||
|
||||
/// Get the parent Spaces of a child room, if any.
|
||||
///
|
||||
/// Only direct parent spaces are returned. Nested sub-space cascading
|
||||
/// is not supported (see design doc requirement 6).
|
||||
#[implement(Service)]
|
||||
pub async fn get_parent_spaces(&self, room_id: &RoomId) -> Vec<OwnedRoomId> {
|
||||
if !self.is_enabled() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
self.room_to_space
|
||||
let all_parents: Vec<OwnedRoomId> = self
|
||||
.room_to_space
|
||||
.read()
|
||||
.await
|
||||
.get(room_id)
|
||||
.map(|set| set.iter().cloned().collect())
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut enabled_parents = Vec::new();
|
||||
for parent in all_parents {
|
||||
if self.is_enabled_for_space(&parent).await {
|
||||
enabled_parents.push(parent);
|
||||
}
|
||||
}
|
||||
enabled_parents
|
||||
}
|
||||
|
||||
/// Get all child rooms of a Space from the forward index.
|
||||
#[implement(Service)]
|
||||
pub async fn get_child_rooms(&self, space_id: &RoomId) -> Vec<OwnedRoomId> {
|
||||
self.space_to_rooms
|
||||
|
|
@ -496,15 +481,12 @@ pub async fn get_child_rooms(&self, space_id: &RoomId) -> Vec<OwnedRoomId> {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Synchronize power levels in a child room based on Space roles.
|
||||
/// This overrides per-room power levels with Space-granted levels.
|
||||
#[implement(Service)]
|
||||
pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Result {
|
||||
if !self.is_enabled() {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if server user is joined to the room
|
||||
let server_user = self.services.globals.server_user.as_ref();
|
||||
if !self
|
||||
.services
|
||||
|
|
@ -516,7 +498,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// 1. Get current power levels for the room
|
||||
let mut power_levels_content: RoomPowerLevelsEventContent = self
|
||||
.services
|
||||
.state_accessor
|
||||
|
|
@ -524,7 +505,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// 2. Get all members of the room
|
||||
let members: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -533,7 +513,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
.collect()
|
||||
.await;
|
||||
|
||||
// 3. For each member, check their space role power level
|
||||
let mut changed = false;
|
||||
for user_id in &members {
|
||||
if user_id == server_user {
|
||||
|
|
@ -547,7 +526,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
.copied()
|
||||
.unwrap_or(power_levels_content.users_default);
|
||||
|
||||
// 4. If the space PL differs from room PL, update it
|
||||
if current_pl != space_pl_int {
|
||||
power_levels_content
|
||||
.users
|
||||
|
|
@ -555,7 +533,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
changed = true;
|
||||
}
|
||||
} else {
|
||||
// Check if any other parent space manages this user's PL
|
||||
let parents = self.get_parent_spaces(room_id).await;
|
||||
let mut managed_by_other = false;
|
||||
for parent in &parents {
|
||||
|
|
@ -575,7 +552,6 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
}
|
||||
}
|
||||
|
||||
// 5. If changed, send updated power levels event
|
||||
if changed {
|
||||
let state_lock = self.services.state.mutex.lock(room_id).await;
|
||||
|
||||
|
|
@ -593,28 +569,20 @@ pub async fn sync_power_levels(&self, space_id: &RoomId, room_id: &RoomId) -> Re
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Auto-join a user to all qualifying child rooms of a Space.
|
||||
///
|
||||
/// Iterates over all child rooms in the `space_to_rooms` forward index,
|
||||
/// checks whether the user qualifies via their assigned roles, and
|
||||
/// force-joins them if they are not already a member.
|
||||
#[implement(Service)]
|
||||
pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &UserId) -> Result {
|
||||
if !self.is_enabled() {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Skip server user — it doesn't need role-based auto-join
|
||||
let server_user = self.services.globals.server_user.as_ref();
|
||||
if user_id == server_user {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Get all child rooms via the space_to_rooms forward index
|
||||
let child_rooms = self.get_child_rooms(space_id).await;
|
||||
|
||||
for child_room_id in &child_rooms {
|
||||
// Skip if already joined
|
||||
if self
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -624,7 +592,6 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check if user qualifies
|
||||
if !self
|
||||
.user_qualifies_for_room(space_id, child_room_id, user_id)
|
||||
.await
|
||||
|
|
@ -632,7 +599,6 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check if server user is joined to the child room
|
||||
if !self
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -645,7 +611,6 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
|
||||
let state_lock = self.services.state.mutex.lock(child_room_id).await;
|
||||
|
||||
// First invite the user (server user as sender)
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
|
|
@ -664,7 +629,6 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
continue;
|
||||
}
|
||||
|
||||
// Then join (user as sender)
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
|
|
@ -686,12 +650,6 @@ pub async fn auto_join_qualifying_rooms(&self, space_id: &RoomId, user_id: &User
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle a state event change that may require enforcement.
|
||||
///
|
||||
/// Spawns a background task (gated by the enforcement semaphore) to
|
||||
/// repopulate the cache and trigger enforcement actions based on the
|
||||
/// event type. Deduplicated per-space to avoid redundant work during
|
||||
/// bulk operations.
|
||||
impl Service {
|
||||
pub fn handle_state_event_change(
|
||||
self: &Arc<Self>,
|
||||
|
|
@ -699,14 +657,13 @@ impl Service {
|
|||
event_type: String,
|
||||
state_key: String,
|
||||
) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
self.server.runtime().spawn(async move {
|
||||
// Deduplicate: if enforcement is already pending for this space, skip.
|
||||
// The running task's populate_space will pick up the latest state.
|
||||
if event_type != SPACE_CASCADING_EVENT_TYPE
|
||||
&& !this.is_enabled_for_space(&space_id).await
|
||||
{
|
||||
return;
|
||||
}
|
||||
{
|
||||
let mut pending = this.pending_enforcement.write().await;
|
||||
if pending.contains(&space_id) {
|
||||
|
|
@ -719,19 +676,16 @@ impl Service {
|
|||
return;
|
||||
};
|
||||
|
||||
// Always repopulate cache first
|
||||
this.populate_space(&space_id).await;
|
||||
|
||||
match event_type.as_str() {
|
||||
| SPACE_ROLES_EVENT_TYPE => {
|
||||
// Role definitions changed — sync PLs in all child rooms
|
||||
let child_rooms = this.get_child_rooms(&space_id).await;
|
||||
for child_room_id in &child_rooms {
|
||||
if let Err(e) = this.sync_power_levels(&space_id, child_room_id).await {
|
||||
debug_warn!(room_id = %child_room_id, error = ?e, "Failed to sync power levels");
|
||||
}
|
||||
}
|
||||
// Revalidate all space members against all child rooms
|
||||
let space_members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -748,7 +702,6 @@ impl Service {
|
|||
}
|
||||
},
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE => {
|
||||
// User's roles changed — auto-join/kick + PL sync
|
||||
if let Ok(user_id) = UserId::parse(state_key.as_str()) {
|
||||
if let Err(e) = this.auto_join_qualifying_rooms(&space_id, user_id).await
|
||||
{
|
||||
|
|
@ -759,7 +712,6 @@ impl Service {
|
|||
{
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Space role auto-kick failed");
|
||||
}
|
||||
// Sync power levels in all child rooms
|
||||
let child_rooms = this.get_child_rooms(&space_id).await;
|
||||
for child_room_id in &child_rooms {
|
||||
if let Err(e) = this.sync_power_levels(&space_id, child_room_id).await
|
||||
|
|
@ -770,7 +722,6 @@ impl Service {
|
|||
}
|
||||
},
|
||||
| SPACE_ROLE_ROOM_EVENT_TYPE => {
|
||||
// Room requirements changed — kick unqualified members
|
||||
if let Ok(target_room) = RoomId::parse(state_key.as_str()) {
|
||||
let members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
|
|
@ -797,33 +748,24 @@ impl Service {
|
|||
| _ => {},
|
||||
}
|
||||
|
||||
// Remove from pending set so future events can trigger enforcement
|
||||
this.pending_enforcement.write().await.remove(&space_id);
|
||||
});
|
||||
}
|
||||
|
||||
/// Handle a new `m.space.child` event — update index and auto-join
|
||||
/// qualifying members.
|
||||
///
|
||||
/// If the child event's `via` field is empty the child is removed from
|
||||
/// both the forward and reverse indexes. Otherwise the child is added
|
||||
/// and all qualifying space members are auto-joined.
|
||||
pub fn handle_space_child_change(
|
||||
self: &Arc<Self>,
|
||||
space_id: OwnedRoomId,
|
||||
child_room_id: OwnedRoomId,
|
||||
) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
self.server.runtime().spawn(async move {
|
||||
if !this.is_enabled_for_space(&space_id).await {
|
||||
return;
|
||||
}
|
||||
let Ok(_permit) = this.enforcement_semaphore.acquire().await else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Read the actual m.space.child state event to check via
|
||||
let child_event_type = StateEventType::SpaceChild;
|
||||
let is_removal = match this
|
||||
.services
|
||||
|
|
@ -840,8 +782,6 @@ impl Service {
|
|||
};
|
||||
|
||||
if is_removal {
|
||||
// Lock ordering: room_to_space before space_to_rooms.
|
||||
// This order must be consistent to avoid deadlocks.
|
||||
let mut room_to_space = this.room_to_space.write().await;
|
||||
if let Some(parents) = room_to_space.get_mut(&child_room_id) {
|
||||
parents.remove(&space_id);
|
||||
|
|
@ -849,7 +789,6 @@ impl Service {
|
|||
room_to_space.remove(&child_room_id);
|
||||
}
|
||||
}
|
||||
// Remove child from space_to_rooms forward index
|
||||
let mut space_to_rooms = this.space_to_rooms.write().await;
|
||||
if let Some(children) = space_to_rooms.get_mut(&space_id) {
|
||||
children.remove(&child_room_id);
|
||||
|
|
@ -857,7 +796,6 @@ impl Service {
|
|||
return;
|
||||
}
|
||||
|
||||
// Add child to reverse index
|
||||
this.room_to_space
|
||||
.write()
|
||||
.await
|
||||
|
|
@ -865,7 +803,6 @@ impl Service {
|
|||
.or_default()
|
||||
.insert(space_id.clone());
|
||||
|
||||
// Add child to forward index
|
||||
this.space_to_rooms
|
||||
.write()
|
||||
.await
|
||||
|
|
@ -873,7 +810,6 @@ impl Service {
|
|||
.or_default()
|
||||
.insert(child_room_id.clone());
|
||||
|
||||
// Check if server user is joined to the child room before enforcement
|
||||
let server_user = this.services.globals.server_user.as_ref();
|
||||
if !this
|
||||
.services
|
||||
|
|
@ -885,7 +821,6 @@ impl Service {
|
|||
return;
|
||||
}
|
||||
|
||||
// Auto-join qualifying space members to this specific child room
|
||||
let space_members: Vec<OwnedUserId> = this
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -908,7 +843,6 @@ impl Service {
|
|||
let state_lock =
|
||||
this.services.state.mutex.lock(&child_room_id).await;
|
||||
|
||||
// Invite
|
||||
if let Err(e) = this
|
||||
.services
|
||||
.timeline
|
||||
|
|
@ -929,7 +863,6 @@ impl Service {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Join
|
||||
if let Err(e) = this
|
||||
.services
|
||||
.timeline
|
||||
|
|
@ -954,28 +887,21 @@ impl Service {
|
|||
});
|
||||
}
|
||||
|
||||
/// Handle a user joining a Space — auto-join them to qualifying child
|
||||
/// rooms.
|
||||
///
|
||||
/// Spawns a background task that auto-joins the user into every child
|
||||
/// room they qualify for, then synchronizes their power levels across
|
||||
/// all child rooms.
|
||||
pub fn handle_space_member_join(
|
||||
self: &Arc<Self>,
|
||||
space_id: OwnedRoomId,
|
||||
user_id: OwnedUserId,
|
||||
) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if the user is the server user
|
||||
if user_id == self.services.globals.server_user {
|
||||
return;
|
||||
}
|
||||
|
||||
let this = Arc::clone(self);
|
||||
self.server.runtime().spawn(async move {
|
||||
if !this.is_enabled_for_space(&space_id).await {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(_permit) = this.enforcement_semaphore.acquire().await else {
|
||||
return;
|
||||
};
|
||||
|
|
@ -983,7 +909,6 @@ impl Service {
|
|||
if let Err(e) = this.auto_join_qualifying_rooms(&space_id, &user_id).await {
|
||||
debug_warn!(user_id = %user_id, error = ?e, "Auto-join on Space join failed");
|
||||
}
|
||||
// Also sync their power levels
|
||||
let child_rooms = this.get_child_rooms(&space_id).await;
|
||||
for child_room_id in &child_rooms {
|
||||
if let Err(e) = this.sync_power_levels(&space_id, child_room_id).await {
|
||||
|
|
@ -994,14 +919,9 @@ impl Service {
|
|||
}
|
||||
}
|
||||
|
||||
/// Remove a user from all child rooms they no longer qualify for.
|
||||
///
|
||||
/// Iterates over child rooms that have role requirements for the given
|
||||
/// space, checks whether the user still qualifies, and kicks them with a
|
||||
/// reason if they do not.
|
||||
#[implement(Service)]
|
||||
pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &UserId) -> Result {
|
||||
if !self.is_enabled() {
|
||||
if !self.is_enabled_for_space(space_id).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
|
@ -1010,7 +930,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
// Get child rooms that have requirements
|
||||
let child_rooms: Vec<OwnedRoomId> = self
|
||||
.room_requirements
|
||||
.read()
|
||||
|
|
@ -1020,7 +939,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
.unwrap_or_default();
|
||||
|
||||
for child_room_id in &child_rooms {
|
||||
// Check if server user is joined to the child room
|
||||
if !self
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -1030,7 +948,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
debug_warn!(room_id = %child_room_id, "Server user is not joined, skipping kick enforcement");
|
||||
continue;
|
||||
}
|
||||
// Skip if not joined
|
||||
if !self
|
||||
.services
|
||||
.state_cache
|
||||
|
|
@ -1040,7 +957,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check if user still qualifies
|
||||
if self
|
||||
.user_qualifies_for_room(space_id, child_room_id, user_id)
|
||||
.await
|
||||
|
|
@ -1048,7 +964,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
continue;
|
||||
}
|
||||
|
||||
// Get existing member event content for the kick
|
||||
let Ok(member_content) = self
|
||||
.services
|
||||
.state_accessor
|
||||
|
|
@ -1061,7 +976,6 @@ pub async fn kick_unqualified_from_rooms(&self, space_id: &RoomId, user_id: &Use
|
|||
|
||||
let state_lock = self.services.state.mutex.lock(child_room_id).await;
|
||||
|
||||
// Kick the user by setting membership to Leave with a reason
|
||||
if let Err(e) = self
|
||||
.services
|
||||
.timeline
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ use conduwuit_core::{
|
|||
event::Event,
|
||||
pdu::{PduCount, PduEvent, PduId, RawPduId},
|
||||
space_roles::{
|
||||
SPACE_ROLE_MEMBER_EVENT_TYPE, SPACE_ROLE_ROOM_EVENT_TYPE, SPACE_ROLES_EVENT_TYPE,
|
||||
SPACE_CASCADING_EVENT_TYPE, SPACE_ROLE_MEMBER_EVENT_TYPE, SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||
SPACE_ROLES_EVENT_TYPE,
|
||||
},
|
||||
},
|
||||
utils::{self, ReadyExt},
|
||||
|
|
@ -362,54 +363,49 @@ where
|
|||
| _ => {},
|
||||
}
|
||||
|
||||
// Space permission cascading: react to role-related state events
|
||||
if self.services.roles.is_enabled() {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
let event_type_str = pdu.event_type().to_string();
|
||||
match event_type_str.as_str() {
|
||||
| SPACE_ROLES_EVENT_TYPE
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE
|
||||
| SPACE_ROLE_ROOM_EVENT_TYPE => {
|
||||
if matches!(
|
||||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> =
|
||||
Arc::clone(&*self.services.roles);
|
||||
roles.handle_state_event_change(
|
||||
room_id.to_owned(),
|
||||
event_type_str,
|
||||
state_key.to_owned(),
|
||||
);
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
}
|
||||
// Handle m.space.child changes
|
||||
if *pdu.kind() == TimelineEventType::SpaceChild {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
if let Ok(child_room_id) = ruma::RoomId::parse(state_key) {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
let event_type_str = pdu.event_type().to_string();
|
||||
match event_type_str.as_str() {
|
||||
| SPACE_ROLES_EVENT_TYPE
|
||||
| SPACE_ROLE_MEMBER_EVENT_TYPE
|
||||
| SPACE_ROLE_ROOM_EVENT_TYPE
|
||||
| SPACE_CASCADING_EVENT_TYPE => {
|
||||
if matches!(
|
||||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> =
|
||||
Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_child_change(room_id.to_owned(), child_room_id.to_owned());
|
||||
roles.handle_state_event_change(
|
||||
room_id.to_owned(),
|
||||
event_type_str,
|
||||
state_key.to_owned(),
|
||||
);
|
||||
}
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
}
|
||||
if *pdu.kind() == TimelineEventType::SpaceChild {
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
if let Ok(child_room_id) = ruma::RoomId::parse(state_key) {
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_child_change(room_id.to_owned(), child_room_id.to_owned());
|
||||
}
|
||||
}
|
||||
// Handle m.room.member join in a Space — auto-join child rooms
|
||||
if *pdu.kind() == TimelineEventType::RoomMember
|
||||
&& let Some(state_key) = pdu.state_key()
|
||||
&& let Ok(content) =
|
||||
pdu.get_content::<ruma::events::room::member::RoomMemberEventContent>()
|
||||
&& content.membership == ruma::events::room::member::MembershipState::Join
|
||||
&& let Ok(user_id) = UserId::parse(state_key)
|
||||
&& matches!(
|
||||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_member_join(room_id.to_owned(), user_id.to_owned());
|
||||
}
|
||||
}
|
||||
if *pdu.kind() == TimelineEventType::RoomMember
|
||||
&& let Some(state_key) = pdu.state_key()
|
||||
&& let Ok(content) =
|
||||
pdu.get_content::<ruma::events::room::member::RoomMemberEventContent>()
|
||||
&& content.membership == ruma::events::room::member::MembershipState::Join
|
||||
&& let Ok(user_id) = UserId::parse(state_key)
|
||||
&& matches!(
|
||||
self.services.state_accessor.get_room_type(room_id).await,
|
||||
Ok(ruma::room::RoomType::Space)
|
||||
) {
|
||||
let roles: Arc<crate::rooms::roles::Service> = Arc::clone(&*self.services.roles);
|
||||
roles.handle_space_member_join(room_id.to_owned(), user_id.to_owned());
|
||||
}
|
||||
|
||||
// CONCERN: If we receive events with a relation out-of-order, we never write
|
||||
|
|
|
|||
|
|
@ -108,8 +108,7 @@ pub async fn build_and_append_pdu(
|
|||
BTreeMap<String, RoleDefinition>,
|
||||
);
|
||||
|
||||
if self.services.roles.is_enabled()
|
||||
&& *pdu.kind() == TimelineEventType::RoomPowerLevels
|
||||
if *pdu.kind() == TimelineEventType::RoomPowerLevels
|
||||
&& pdu.sender()
|
||||
!= <OwnedUserId as AsRef<UserId>>::as_ref(&self.services.globals.server_user)
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue