feat(spaces): add admin commands for space role management
Add !admin space roles subcommands: list, add, remove, assign, revoke, require, unrequire, user, room, enable, disable, status. Role definitions, assignments, and room requirements are managed via state events. Enable/disable controls per-space cascading override.
This commit is contained in:
parent
f4ab456bbd
commit
4b37877bc3
4 changed files with 649 additions and 0 deletions
|
|
@ -11,6 +11,7 @@ use crate::{
|
||||||
query::{self, QueryCommand},
|
query::{self, QueryCommand},
|
||||||
room::{self, RoomCommand},
|
room::{self, RoomCommand},
|
||||||
server::{self, ServerCommand},
|
server::{self, ServerCommand},
|
||||||
|
space::{self, SpaceCommand},
|
||||||
token::{self, TokenCommand},
|
token::{self, TokenCommand},
|
||||||
user::{self, UserCommand},
|
user::{self, UserCommand},
|
||||||
};
|
};
|
||||||
|
|
@ -34,6 +35,10 @@ pub enum AdminCommand {
|
||||||
/// Commands for managing rooms
|
/// Commands for managing rooms
|
||||||
Rooms(RoomCommand),
|
Rooms(RoomCommand),
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
/// Commands for managing space permissions
|
||||||
|
Spaces(SpaceCommand),
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
/// Commands for managing federation
|
/// Commands for managing federation
|
||||||
Federation(FederationCommand),
|
Federation(FederationCommand),
|
||||||
|
|
@ -81,6 +86,10 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
||||||
token::process(command, context).await
|
token::process(command, context).await
|
||||||
},
|
},
|
||||||
| Rooms(command) => room::process(command, context).await,
|
| Rooms(command) => room::process(command, context).await,
|
||||||
|
| Spaces(command) => {
|
||||||
|
context.bail_restricted()?;
|
||||||
|
space::process(command, context).await
|
||||||
|
},
|
||||||
| Federation(command) => federation::process(command, context).await,
|
| Federation(command) => federation::process(command, context).await,
|
||||||
| Server(command) => server::process(command, context).await,
|
| Server(command) => server::process(command, context).await,
|
||||||
| Debug(command) => debug::process(command, context).await,
|
| Debug(command) => debug::process(command, context).await,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ pub(crate) mod media;
|
||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
pub(crate) mod room;
|
pub(crate) mod room;
|
||||||
pub(crate) mod server;
|
pub(crate) mod server;
|
||||||
|
pub(crate) mod space;
|
||||||
pub(crate) mod token;
|
pub(crate) mod token;
|
||||||
pub(crate) mod user;
|
pub(crate) mod user;
|
||||||
|
|
||||||
|
|
|
||||||
15
src/admin/space/mod.rs
Normal file
15
src/admin/space/mod.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
pub(super) mod roles;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
use conduwuit::Result;
|
||||||
|
|
||||||
|
use self::roles::SpaceRolesCommand;
|
||||||
|
use crate::admin_command_dispatch;
|
||||||
|
|
||||||
|
#[admin_command_dispatch]
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum SpaceCommand {
|
||||||
|
#[command(subcommand)]
|
||||||
|
/// Manage space roles and permissions
|
||||||
|
Roles(SpaceRolesCommand),
|
||||||
|
}
|
||||||
624
src/admin/space/roles.rs
Normal file
624
src/admin/space/roles.rs
Normal file
|
|
@ -0,0 +1,624 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
use conduwuit::{Err, Event, Result, matrix::pdu::PduBuilder};
|
||||||
|
use conduwuit_core::matrix::space_roles::{
|
||||||
|
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};
|
||||||
|
use serde_json::value::to_raw_value;
|
||||||
|
|
||||||
|
use crate::{admin_command, admin_command_dispatch};
|
||||||
|
|
||||||
|
fn roles_event_type() -> StateEventType {
|
||||||
|
StateEventType::from(SPACE_ROLES_EVENT_TYPE.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn member_event_type() -> StateEventType {
|
||||||
|
StateEventType::from(SPACE_ROLE_MEMBER_EVENT_TYPE.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn room_event_type() -> StateEventType {
|
||||||
|
StateEventType::from(SPACE_ROLE_ROOM_EVENT_TYPE.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cascading_event_type() -> StateEventType {
|
||||||
|
StateEventType::from(SPACE_CASCADING_EVENT_TYPE.to_owned())
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! resolve_room_as_space {
|
||||||
|
($self:expr, $space:expr) => {{
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
space_id
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! resolve_space {
|
||||||
|
($self:expr, $space:expr) => {{
|
||||||
|
let space_id = resolve_room_as_space!($self, $space);
|
||||||
|
if !$self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.roles
|
||||||
|
.is_enabled_for_space(&space_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return $self
|
||||||
|
.write_str(
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
space_id
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! custom_state_pdu {
|
||||||
|
($event_type:expr, $state_key:expr, $content:expr) => {
|
||||||
|
PduBuilder {
|
||||||
|
event_type: $event_type.to_owned().into(),
|
||||||
|
content: to_raw_value($content)
|
||||||
|
.map_err(|e| conduwuit::err!("Failed to serialize state event content: {e}"))?,
|
||||||
|
state_key: Some($state_key.to_owned().into()),
|
||||||
|
..PduBuilder::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! send_space_state {
|
||||||
|
($self:expr, $space_id:expr, $event_type:expr, $state_key:expr, $content:expr) => {{
|
||||||
|
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!($event_type, $state_key, $content),
|
||||||
|
server_user,
|
||||||
|
Some(&$space_id),
|
||||||
|
&state_lock,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command_dispatch]
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum SpaceRolesCommand {
|
||||||
|
/// List all roles defined in a space
|
||||||
|
List {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
},
|
||||||
|
/// Add a new role to a space
|
||||||
|
Add {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
role_name: String,
|
||||||
|
#[arg(long)]
|
||||||
|
description: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
power_level: Option<i64>,
|
||||||
|
},
|
||||||
|
/// Remove a role from a space
|
||||||
|
Remove {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
role_name: String,
|
||||||
|
},
|
||||||
|
/// Assign a role to a user
|
||||||
|
Assign {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
role_name: String,
|
||||||
|
},
|
||||||
|
/// Revoke a role from a user
|
||||||
|
Revoke {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
role_name: String,
|
||||||
|
},
|
||||||
|
/// Require a role for a room
|
||||||
|
Require {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
role_name: String,
|
||||||
|
},
|
||||||
|
/// Remove a role requirement from a room
|
||||||
|
Unrequire {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
role_name: String,
|
||||||
|
},
|
||||||
|
/// Show a user's roles in a space
|
||||||
|
User {
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
},
|
||||||
|
/// Show a room's role requirements in a space
|
||||||
|
Room {
|
||||||
|
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]
|
||||||
|
async fn list(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
let roles_event_type = roles_event_type();
|
||||||
|
|
||||||
|
let content: SpaceRolesEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if content.roles.is_empty() {
|
||||||
|
return self.write_str("No roles defined in this space.").await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut msg = format!("Roles in {space_id}:\n```\n");
|
||||||
|
for (name, def) in &content.roles {
|
||||||
|
let pl = def
|
||||||
|
.power_level
|
||||||
|
.map(|p| format!(" (power_level: {p})"))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let _ = writeln!(msg, "- {name}: {}{pl}", def.description);
|
||||||
|
}
|
||||||
|
msg.push_str("```");
|
||||||
|
|
||||||
|
self.write_str(&msg).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn add(
|
||||||
|
&self,
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
role_name: String,
|
||||||
|
description: Option<String>,
|
||||||
|
power_level: Option<i64>,
|
||||||
|
) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
|
||||||
|
if let Some(pl) = power_level {
|
||||||
|
if pl > i64::from(ruma::Int::MAX) || pl < i64::from(ruma::Int::MIN) {
|
||||||
|
return Err!(
|
||||||
|
"Power level must be between {} and {}.",
|
||||||
|
ruma::Int::MIN,
|
||||||
|
ruma::Int::MAX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let roles_event_type = roles_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRolesEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if content.roles.contains_key(&role_name) {
|
||||||
|
return Err!("Role '{role_name}' already exists in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.roles.insert(role_name.clone(), RoleDefinition {
|
||||||
|
description: description.unwrap_or_else(|| role_name.clone()),
|
||||||
|
power_level,
|
||||||
|
});
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||||
|
|
||||||
|
self.write_str(&format!("Added role '{role_name}' to space {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn remove(&self, space: OwnedRoomOrAliasId, role_name: String) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
let roles_event_type = roles_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRolesEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if content.roles.remove(&role_name).is_none() {
|
||||||
|
return Err!("Role '{role_name}' does not exist in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLES_EVENT_TYPE, "", &content);
|
||||||
|
|
||||||
|
let member_event_type = member_event_type();
|
||||||
|
let server_user = &self.services.globals.server_user;
|
||||||
|
if let Ok(shortstatehash) = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state
|
||||||
|
.get_room_shortstatehash(&space_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
let state_lock = self.services.rooms.state.mutex.lock(&space_id).await;
|
||||||
|
let user_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.state_keys_with_ids(shortstatehash, &member_event_type)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for (state_key, event_id) in user_entries {
|
||||||
|
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||||
|
if let Ok(mut member_content) = pdu.get_content::<SpaceRoleMemberEventContent>() {
|
||||||
|
if member_content.roles.contains(&role_name) {
|
||||||
|
member_content.roles.retain(|r| r != &role_name);
|
||||||
|
self.services
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
custom_state_pdu!(
|
||||||
|
SPACE_ROLE_MEMBER_EVENT_TYPE,
|
||||||
|
&state_key,
|
||||||
|
&member_content
|
||||||
|
),
|
||||||
|
server_user,
|
||||||
|
Some(&space_id),
|
||||||
|
&state_lock,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cascade: remove the role from all rooms' role requirement events
|
||||||
|
let room_event_type = room_event_type();
|
||||||
|
|
||||||
|
let room_entries: Vec<(_, ruma::OwnedEventId)> = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.state_keys_with_ids(shortstatehash, &room_event_type)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
for (state_key, event_id) in room_entries {
|
||||||
|
if let Ok(pdu) = self.services.rooms.timeline.get_pdu(&event_id).await {
|
||||||
|
if let Ok(mut room_content) = pdu.get_content::<SpaceRoleRoomEventContent>() {
|
||||||
|
if room_content.required_roles.contains(&role_name) {
|
||||||
|
room_content.required_roles.retain(|r| r != &role_name);
|
||||||
|
self.services
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
custom_state_pdu!(
|
||||||
|
SPACE_ROLE_ROOM_EVENT_TYPE,
|
||||||
|
&state_key,
|
||||||
|
&room_content
|
||||||
|
),
|
||||||
|
server_user,
|
||||||
|
Some(&space_id),
|
||||||
|
&state_lock,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.write_str(&format!("Removed role '{role_name}' from space {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn assign(
|
||||||
|
&self,
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
role_name: String,
|
||||||
|
) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
|
||||||
|
let roles_event_type = roles_event_type();
|
||||||
|
let role_defs: SpaceRolesEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !role_defs.roles.contains_key(&role_name) {
|
||||||
|
return Err!("Role '{role_name}' does not exist in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let member_event_type = member_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRoleMemberEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if content.roles.contains(&role_name) {
|
||||||
|
return Err!("User {user_id} already has role '{role_name}' in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.roles.push(role_name.clone());
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||||
|
|
||||||
|
self.write_str(&format!("Assigned role '{role_name}' to {user_id} in space {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn revoke(
|
||||||
|
&self,
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
user_id: OwnedUserId,
|
||||||
|
role_name: String,
|
||||||
|
) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
let member_event_type = member_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRoleMemberEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &member_event_type, user_id.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let original_len = content.roles.len();
|
||||||
|
content.roles.retain(|r| r != &role_name);
|
||||||
|
|
||||||
|
if content.roles.len() == original_len {
|
||||||
|
return Err!("User {user_id} does not have role '{role_name}' in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLE_MEMBER_EVENT_TYPE, user_id.as_str(), &content);
|
||||||
|
|
||||||
|
self.write_str(&format!("Revoked role '{role_name}' from {user_id} in space {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn require(
|
||||||
|
&self,
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
role_name: String,
|
||||||
|
) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
|
||||||
|
let child_rooms = self.services.rooms.roles.get_child_rooms(&space_id).await;
|
||||||
|
if !child_rooms.contains(&room_id) {
|
||||||
|
return Err!("Room {room_id} is not a child of space {space_id}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let roles_event_type = roles_event_type();
|
||||||
|
let role_defs: SpaceRolesEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &roles_event_type, "")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !role_defs.roles.contains_key(&role_name) {
|
||||||
|
return Err!("Role '{role_name}' does not exist in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let room_event_type = room_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRoleRoomEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if content.required_roles.contains(&role_name) {
|
||||||
|
return Err!("Room {room_id} already requires role '{role_name}' in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
content.required_roles.push(role_name.clone());
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||||
|
|
||||||
|
self.write_str(&format!(
|
||||||
|
"Room {room_id} now requires role '{role_name}' in space {space_id}."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn unrequire(
|
||||||
|
&self,
|
||||||
|
space: OwnedRoomOrAliasId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
role_name: String,
|
||||||
|
) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
let room_event_type = room_event_type();
|
||||||
|
|
||||||
|
let mut content: SpaceRoleRoomEventContent = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content(&space_id, &room_event_type, room_id.as_str())
|
||||||
|
.await
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let original_len = content.required_roles.len();
|
||||||
|
content.required_roles.retain(|r| r != &role_name);
|
||||||
|
|
||||||
|
if content.required_roles.len() == original_len {
|
||||||
|
return Err!("Room {room_id} does not require role '{role_name}' in this space.");
|
||||||
|
}
|
||||||
|
|
||||||
|
send_space_state!(self, space_id, SPACE_ROLE_ROOM_EVENT_TYPE, room_id.as_str(), &content);
|
||||||
|
|
||||||
|
self.write_str(&format!(
|
||||||
|
"Removed role requirement '{role_name}' from room {room_id} in space {space_id}."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn user(&self, space: OwnedRoomOrAliasId, user_id: OwnedUserId) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
|
||||||
|
let roles = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.roles
|
||||||
|
.get_user_roles_in_space(&space_id, &user_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match roles {
|
||||||
|
| Some(roles) if !roles.is_empty() => {
|
||||||
|
let list: String = roles
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("- {r}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
self.write_str(&format!("Roles for {user_id} in space {space_id}:\n```\n{list}\n```"))
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
| _ =>
|
||||||
|
self.write_str(&format!("User {user_id} has no roles in space {space_id}."))
|
||||||
|
.await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn room(&self, space: OwnedRoomOrAliasId, room_id: OwnedRoomId) -> Result {
|
||||||
|
let space_id = resolve_space!(self, space);
|
||||||
|
|
||||||
|
let reqs = self
|
||||||
|
.services
|
||||||
|
.rooms
|
||||||
|
.roles
|
||||||
|
.get_room_requirements_in_space(&space_id, &room_id)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match reqs {
|
||||||
|
| Some(reqs) if !reqs.is_empty() => {
|
||||||
|
let list: String = reqs
|
||||||
|
.iter()
|
||||||
|
.map(|r| format!("- {r}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
|
self.write_str(&format!(
|
||||||
|
"Required roles for room {room_id} in space {space_id}:\n```\n{list}\n```"
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
| _ =>
|
||||||
|
self.write_str(&format!(
|
||||||
|
"Room {room_id} has no role requirements in space {space_id}."
|
||||||
|
))
|
||||||
|
.await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn enable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||||
|
let space_id = resolve_room_as_space!(self, space);
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.rooms
|
||||||
|
.roles
|
||||||
|
.ensure_default_roles(&space_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let content = SpaceCascadingEventContent { enabled: true };
|
||||||
|
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||||
|
|
||||||
|
self.write_str(&format!("Space permission cascading enabled for {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn disable(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||||
|
let space_id = resolve_room_as_space!(self, space);
|
||||||
|
|
||||||
|
let content = SpaceCascadingEventContent { enabled: false };
|
||||||
|
send_space_state!(self, space_id, SPACE_CASCADING_EVENT_TYPE, "", &content);
|
||||||
|
|
||||||
|
self.write_str(&format!("Space permission cascading disabled for {space_id}."))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
async fn status(&self, space: OwnedRoomOrAliasId) -> Result {
|
||||||
|
let space_id = resolve_room_as_space!(self, space);
|
||||||
|
|
||||||
|
let global_default = self.services.rooms.roles.is_enabled();
|
||||||
|
let cascading_event_type = cascading_event_type();
|
||||||
|
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
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue