From 693f80eeb295d9de0f52067c2e9cc9c0867d1306 Mon Sep 17 00:00:00 2001 From: timedout Date: Sun, 1 Nov 2026 02:00:53 +0000 Subject: [PATCH] feat: Move & merge some membership-related commands into `rooms membership` --- src/admin/room/membership.rs | 117 +++++++++++++++++++++++++++++ src/admin/room/mod.rs | 10 ++- src/admin/user/commands.rs | 112 +-------------------------- src/admin/user/mod.rs | 26 ------- src/api/client/membership/leave.rs | 2 +- 5 files changed, 129 insertions(+), 138 deletions(-) create mode 100644 src/admin/room/membership.rs diff --git a/src/admin/room/membership.rs b/src/admin/room/membership.rs new file mode 100644 index 000000000..cc69a465d --- /dev/null +++ b/src/admin/room/membership.rs @@ -0,0 +1,117 @@ +use std::hash::RandomState; + +use clap::Subcommand; +use conduwuit::{Err, Result, info}; +use conduwuit_macros::admin_command_dispatch; +use ruma::{OwnedRoomOrAliasId, OwnedServerName, OwnedUserId}; + +#[admin_command_dispatch] +#[derive(Debug, Subcommand)] +pub enum RoomMembershipCommands { + /// Make a local user join a room. + #[clap(name = "join")] + JoinRoom { + /// The user to make join the room. Must be a local user. + user_id: OwnedUserId, + /// The room (ID or alias) to join. + room: OwnedRoomOrAliasId, + /// The server to join through, if a remote room. + /// + /// Residents returned after resolving an alias override this option. + #[arg(long)] + via: Option, + }, + /// Make a local user leave a room or reject an invite. + #[clap(name = "leave", alias = "reject-invite")] + LeaveRoom { + /// The user to make leave the room. Must be a local user. + user_id: OwnedUserId, + /// The room (ID or alias) to leave. + room: OwnedRoomOrAliasId, + /// The server to leave through, if a room we are not a member of (e.g. + /// an invite). + /// + /// Residents returned after resolving an alias override this option. + #[arg(long)] + via: Option, + }, +} + +impl crate::Context<'_> { + pub(super) async fn join_room( + &self, + user_id: OwnedUserId, + room: OwnedRoomOrAliasId, + via: Option, + ) -> Result { + self.bail_restricted()?; + if !self.services.globals.user_is_local(&user_id) { + return Err!("User {user_id:?} does not belong to our server."); + } + let (room_id, residents) = self + .services + .rooms + .alias + .resolve_with_servers(&room, via.map(|v| vec![v])) + .await?; + self.services + .rooms + .membership + .join_room(&user_id, &room_id, None, &residents) + .await?; + self.write_str(&format!("Successfully joined {room_id} as {user_id}.")) + .await + } + + pub(super) async fn leave_room( + &self, + user_id: OwnedUserId, + room: OwnedRoomOrAliasId, + via: Option, + ) -> Result { + self.bail_restricted()?; + if !self.services.globals.user_is_local(&user_id) { + return Err!("User {user_id:?} does not belong to our server."); + } + let no_explicit_via = via.is_none(); + let (room_id, residents) = self + .services + .rooms + .alias + .resolve_with_servers(&room, via.map(|v| vec![v])) + .await?; + if no_explicit_via + || self + .services + .rooms + .state_cache + .server_in_room(self.services.globals.server_name(), &room_id) + .await + { + if !self + .services + .rooms + .state_cache + .is_joined(&user_id, &room_id) + .await + { + return Err!("User {user_id:?} is not a member of room {room_id:?}."); + } + + info!("Leaving room locally..."); + conduwuit_api::client::leave_room(self.services, &user_id, &room_id, None).await?; + } else { + info!("Leaving room remotely..."); + conduwuit_api::client::remote_leave_room::( + self.services, + &user_id, + &room_id, + None, + residents.into_iter().collect(), + ) + .await?; + } + self.write_str(&format!("Successfully made {user_id} leave {room_id}.")) + .await + } +} diff --git a/src/admin/room/mod.rs b/src/admin/room/mod.rs index 026812307..a209a193e 100644 --- a/src/admin/room/mod.rs +++ b/src/admin/room/mod.rs @@ -3,6 +3,7 @@ mod commands; mod directory; mod graph; mod info; +mod membership; mod moderation; mod state; @@ -12,9 +13,10 @@ use ruma::OwnedRoomId; use self::{ alias::RoomAliasCommand, directory::RoomDirectoryCommand, graph::GraphCommand, - info::RoomInfoCommand, moderation::RoomModerationCommand, + info::RoomInfoCommand, membership::RoomMembershipCommands, moderation::RoomModerationCommand, + state::RoomStateCommand, }; -use crate::{admin_command_dispatch, room::state::RoomStateCommand}; +use crate::admin_command_dispatch; #[admin_command_dispatch] #[derive(Debug, Subcommand)] @@ -58,6 +60,10 @@ pub enum RoomCommand { #[command(subcommand)] Directory(RoomDirectoryCommand), + /// Manage room memberships (join, leave, invite, etc.) for local users. + #[command(subcommand)] + Membership(RoomMembershipCommands), + /// Interface with room graphs directly (advanced). #[command(subcommand)] Graph(GraphCommand), diff --git a/src/admin/user/commands.rs b/src/admin/user/commands.rs index fbde37570..a61c6d95a 100644 --- a/src/admin/user/commands.rs +++ b/src/admin/user/commands.rs @@ -1,8 +1,6 @@ -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; -use api::client::{ - full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room, -}; +use api::client::{full_user_deactivate, leave_room, recreate_push_rules_and_return}; use conduwuit::{ Err, Result, debug_warn, info, matrix::{Event, pdu::PartialPdu}, @@ -12,8 +10,7 @@ use conduwuit::{ use futures::{FutureExt, StreamExt}; use lettre::Address; use ruma::{ - OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, ServerName, - UserId, assign, + OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedUserId, UserId, assign, events::{ RoomAccountDataEventType, room::{power_levels::RoomPowerLevelsEventContent, redaction::RoomRedactionEventContent}, @@ -667,69 +664,6 @@ impl crate::Context<'_> { .await } - pub(super) async fn force_join_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, - via: Option, - ) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let (room_id, mut servers) = self - .services - .rooms - .alias - .resolve_with_servers(&room_id, None) - .await?; - if let Some(via) = via.map(ServerName::parse).transpose()? { - servers.retain(|n| *n != via); - servers.insert(0, via); - } - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - self.services - .rooms - .membership - .join_room(&user_id, &room_id, None, &servers) - .await?; - - self.write_str(&format!("{user_id} has been joined to {room_id}.")) - .await - } - - pub(super) async fn force_leave_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, - ) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let room_id = self.services.rooms.alias.resolve(&room_id).await?; - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - - if !self - .services - .rooms - .state_cache - .is_joined(&user_id, &room_id) - .await - { - return Err!("{user_id} is not joined in the room"); - } - - leave_room(self.services, &user_id, &room_id, None) - .boxed() - .await?; - - self.write_str(&format!("{user_id} has left {room_id}.")) - .await - } - pub(super) async fn force_demote( &self, user_id: String, @@ -943,46 +877,6 @@ impl crate::Context<'_> { .await } - pub(super) async fn force_leave_remote_room( - &self, - user_id: String, - room_id: OwnedRoomOrAliasId, - via: Option, - ) -> Result { - let user_id = parse_local_user_id(self.services, &user_id)?; - let (room_id, vias_raw) = self - .services - .rooms - .alias - .resolve_with_servers( - &room_id, - if let Some(v) = via.clone() { - Some(vec![ServerName::parse(v)?]) - } else { - None - }, - ) - .await?; - - assert!( - self.services.globals.user_is_local(&user_id), - "Parsed user_id must be a local user" - ); - let mut vias: HashSet = HashSet::new(); - if let Some(via) = via { - vias.insert(ServerName::parse(via)?); - } - for server in vias_raw { - vias.insert(server); - } - remote_leave_room(self.services, &user_id, &room_id, None, vias) - .boxed() - .await?; - - self.write_str(&format!("{user_id} successfully left {room_id} via remote server.")) - .await - } - pub(super) async fn lock(&self, user_id: String) -> Result { let user_id = parse_active_local_user_id(self.services, &user_id).await?; diff --git a/src/admin/user/mod.rs b/src/admin/user/mod.rs index 3d431b33e..a52910ce3 100644 --- a/src/admin/user/mod.rs +++ b/src/admin/user/mod.rs @@ -193,32 +193,6 @@ pub enum UserCommand { user_id: String, }, - /// Manually join a local user to a room. - ForceJoinRoom { - /// The user to join - user_id: String, - /// The room to join - room_id: OwnedRoomOrAliasId, - /// The server name to join via. - /// - /// This server will always be tried first, however if more are - /// available, they may be tried after. - via: Option, - }, - - /// Manually leave a local user from a room. - ForceLeaveRoom { - user_id: String, - room_id: OwnedRoomOrAliasId, - }, - - /// Manually leave a remote room for a local user. - ForceLeaveRemoteRoom { - user_id: String, - room_id: OwnedRoomOrAliasId, - via: Option, - }, - /// Forces the specified user to drop their power levels to the room /// default, if their permissions allow and the auth check permits ForceDemote { diff --git a/src/api/client/membership/leave.rs b/src/api/client/membership/leave.rs index 1ef0b21fa..86455f1c5 100644 --- a/src/api/client/membership/leave.rs +++ b/src/api/client/membership/leave.rs @@ -214,7 +214,7 @@ pub async fn leave_room( Ok(()) } -pub async fn remote_leave_room( +pub async fn remote_leave_room( services: &Services, user_id: &UserId, room_id: &RoomId,