From e48d92c6354e726fde09674543bc1331503d9cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0spik?= Date: Fri, 12 Jun 2026 23:48:26 +0300 Subject: [PATCH] feat: add admin channel edit endpoint --- crates/core/models/src/v0/admin.rs | 2 + .../admin/channels/actions/channel_edit.rs | 282 ++++++++++++++++++ crates/delta/src/routes/admin/mod.rs | 1 + 3 files changed, 285 insertions(+) diff --git a/crates/core/models/src/v0/admin.rs b/crates/core/models/src/v0/admin.rs index 3703f19c..4d2b6e16 100644 --- a/crates/core/models/src/v0/admin.rs +++ b/crates/core/models/src/v0/admin.rs @@ -328,6 +328,7 @@ auto_derived! { // Channels DeleteChannel, + EditChannel, } // Joiner payloads @@ -378,6 +379,7 @@ impl AdminAuditItemActions { AdminAuditItemActions::DeleteAccount => true, AdminAuditItemActions::DisableAccount => true, AdminAuditItemActions::DeleteChannel => true, + AdminAuditItemActions::EditChannel => true, } } } diff --git a/crates/delta/src/routes/admin/channels/actions/channel_edit.rs b/crates/delta/src/routes/admin/channels/actions/channel_edit.rs index e69de29b..e2b236bd 100644 --- a/crates/delta/src/routes/admin/channels/actions/channel_edit.rs +++ b/crates/delta/src/routes/admin/channels/actions/channel_edit.rs @@ -0,0 +1,282 @@ +use rocket::serde::json::Json; +use crate::routes::admin::util::{ + create_audit_action, flatten_authorized_user, user_has_permission, +}; +use revolt_database::{util::reference::Reference, AdminAuthorization, Channel, Database, File, PartialChannel, SystemMessage, User, AMQP}; +use revolt_models::v0; +use revolt_result::{create_error, Result}; +use rocket::State; +use validator::Validate; +use revolt_database::voice::{delete_voice_channel, UserVoiceChannel, VoiceClient}; + +#[openapi(tag = "Admin")] +#[patch("/admin/channels/?", data= "")] +pub async fn admin_channel_edit( + db: &State, + auth: AdminAuthorization, + voice_client: &State, + amqp: &State, + channel_id: Reference<'_>, + user: User, + data: Json, + case: Option<&str> +) -> Result> { + let data = data.into_inner(); + data.validate().map_err(|error| { + create_error!(FailedValidation { + error: error.to_string() + }) + })?; + + let admin = flatten_authorized_user(&auth); + if !user_has_permission(admin, v0::AdminUserPermissionFlags::ManageChannels) { + return Err(create_error!(MissingPermission { + permission: "ManageChannels".to_string() + })); + } + + let mut channel = channel_id.as_channel(db).await?; + + if data.name.is_none() + && data.description.is_none() + && data.icon.is_none() + && data.nsfw.is_none() + && data.owner.is_none() + && data.voice.is_none() + && data.slowmode.is_none() + && data.remove.is_empty() + { + return Ok(Json(channel.into())); + } + + let mut partial: PartialChannel = Default::default(); + + // Transfer group ownership + if let Some(new_owner) = data.owner { + if let Channel::Group { + owner, recipients, .. + } = &mut channel + { + + // Ensure user is part of group + if !recipients.contains(&new_owner) { + return Err(create_error!(NotInGroup)); + } + + // Transfer ownership + partial.owner = Some(new_owner.to_string()); + let old_owner = std::mem::replace(owner, new_owner.to_string()); + + // Notify clients + SystemMessage::ChannelOwnershipChanged { + from: old_owner, + to: new_owner, + } + } else { + return Err(create_error!(InvalidOperation)); + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + + match &mut channel { + Channel::Group { + id, + name, + description, + icon, + nsfw, + .. + } => { + if data.remove.contains(&v0::FieldsChannel::Icon) { + if let Some(icon) = &icon { + db.mark_attachment_as_deleted(&icon.id).await?; + } + } + + for field in &data.remove { + match field { + v0::FieldsChannel::Description => { + description.take(); + } + v0::FieldsChannel::Icon => { + icon.take(); + } + _ => {} + } + } + + if let Some(icon_id) = data.icon { + partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?); + *icon = partial.icon.clone(); + } + + if let Some(new_name) = data.name { + *name = new_name.clone(); + partial.name = Some(new_name); + } + + if let Some(new_description) = data.description { + partial.description = Some(new_description); + *description = partial.description.clone(); + } + + if let Some(new_nsfw) = data.nsfw { + *nsfw = new_nsfw; + partial.nsfw = Some(new_nsfw); + } + + // Send out mutation system messages. + if let Some(name) = &partial.name { + SystemMessage::ChannelRenamed { + name: name.to_string(), + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + + if partial.description.is_some() { + SystemMessage::ChannelDescriptionChanged { + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + + if partial.icon.is_some() { + SystemMessage::ChannelIconChanged { + by: user.id.clone(), + } + .into_message(channel.id().to_string()) + .send( + db, + Some(amqp), + user.as_author_for_system(), + None, + None, + &channel, + false, + ) + .await + .ok(); + } + } + Channel::TextChannel { + id, + name, + description, + icon, + nsfw, + voice, + slowmode, + .. + } => { + if data.remove.contains(&v0::FieldsChannel::Icon) { + if let Some(icon) = &icon { + db.mark_attachment_as_deleted(&icon.id).await?; + } + } + + for field in &data.remove { + match field { + v0::FieldsChannel::Description => { + description.take(); + } + v0::FieldsChannel::Icon => { + icon.take(); + } + v0::FieldsChannel::Voice => { + voice.take(); + } + _ => {} + } + } + + if let Some(icon_id) = data.icon { + partial.icon = Some(File::use_channel_icon(db, &icon_id, id, &user.id).await?); + *icon = partial.icon.clone(); + } + + if let Some(new_name) = data.name { + *name = new_name.clone(); + partial.name = Some(new_name); + } + + if let Some(new_description) = data.description { + partial.description = Some(new_description); + *description = partial.description.clone(); + } + + if let Some(new_nsfw) = data.nsfw { + *nsfw = new_nsfw; + partial.nsfw = Some(new_nsfw); + } + + if let Some(new_voice) = data.voice { + *voice = Some(new_voice.clone().into()); + partial.voice = Some(new_voice.into()); + } + + if let Some(new_slowmode) = data.slowmode { + *slowmode = Some(new_slowmode); + partial.slowmode = Some(new_slowmode); + } + } + _ => return Err(create_error!(InvalidOperation)), + }; + + channel + .update( + db, + partial, + data.remove.into_iter().map(|f| f.into()).collect(), + ) + .await?; + + if channel.voice().is_none() { + delete_voice_channel(voice_client, &UserVoiceChannel::from_channel(&channel)).await?; + } + + create_audit_action( + &db, + &user.id, + v0::AdminAuditItemActions::EditChannel, + case, + Some(channel_id.id), + None, + ) + .await?; + + Ok(Json(channel.into())) +} \ No newline at end of file diff --git a/crates/delta/src/routes/admin/mod.rs b/crates/delta/src/routes/admin/mod.rs index 347addba..85b51dff 100644 --- a/crates/delta/src/routes/admin/mod.rs +++ b/crates/delta/src/routes/admin/mod.rs @@ -41,5 +41,6 @@ pub fn routes() -> (Vec, OpenApi) { accounts::account_delete::admin_account_delete, accounts::account_disable::admin_account_disable, channels::actions::channel_delete::admin_delete_channel, + channels::actions::channel_edit::admin_channel_edit, ] }