From 0dc544249825a49c793309edee5ec1838458a6da Mon Sep 17 00:00:00 2001 From: Angelo Kontaxis Date: Sun, 25 Jan 2026 19:42:48 +0000 Subject: [PATCH] feat: allow kicking members from voice channels (#495) --- .../src/models/server_members/model.rs | 4 ++- .../src/models/server_members/ops/mongodb.rs | 19 ++++++------ crates/core/database/src/util/bridge/v0.rs | 2 ++ crates/core/models/src/v0/server_members.rs | 1 + .../delta/src/routes/servers/member_edit.rs | 30 +++++++++++++++---- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/core/database/src/models/server_members/model.rs b/crates/core/database/src/models/server_members/model.rs index 74ec04a8..0062a270 100644 --- a/crates/core/database/src/models/server_members/model.rs +++ b/crates/core/database/src/models/server_members/model.rs @@ -72,6 +72,7 @@ auto_derived!( CanReceive, CanPublish, JoinedAt, + VoiceChannel, } /// Member removal intention @@ -224,13 +225,14 @@ impl Member { pub fn remove_field(&mut self, field: &FieldsMember) { match field { - FieldsMember::JoinedAt => (), + FieldsMember::JoinedAt => {}, FieldsMember::Avatar => self.avatar = None, FieldsMember::Nickname => self.nickname = None, FieldsMember::Roles => self.roles.clear(), FieldsMember::Timeout => self.timeout = None, FieldsMember::CanReceive => self.can_receive = true, FieldsMember::CanPublish => self.can_publish = true, + FieldsMember::VoiceChannel => {}, } } diff --git a/crates/core/database/src/models/server_members/ops/mongodb.rs b/crates/core/database/src/models/server_members/ops/mongodb.rs index 198ad6a8..377e5877 100644 --- a/crates/core/database/src/models/server_members/ops/mongodb.rs +++ b/crates/core/database/src/models/server_members/ops/mongodb.rs @@ -330,14 +330,15 @@ impl AbstractServerMembers for MongoDb { impl IntoDocumentPath for FieldsMember { fn as_path(&self) -> Option<&'static str> { - Some(match self { - FieldsMember::JoinedAt => "joined_at", - FieldsMember::Avatar => "avatar", - FieldsMember::Nickname => "nickname", - FieldsMember::Roles => "roles", - FieldsMember::Timeout => "timeout", - FieldsMember::CanPublish => "can_publish", - FieldsMember::CanReceive => "can_receive", - }) + match self { + FieldsMember::JoinedAt => Some("joined_at"), + FieldsMember::Avatar => Some("avatar"), + FieldsMember::Nickname => Some("nickname"), + FieldsMember::Roles => Some("roles"), + FieldsMember::Timeout => Some("timeout"), + FieldsMember::CanPublish => Some("can_publish"), + FieldsMember::CanReceive => Some("can_receive"), + FieldsMember::VoiceChannel => None, + } } } diff --git a/crates/core/database/src/util/bridge/v0.rs b/crates/core/database/src/util/bridge/v0.rs index de63db90..aafb2313 100644 --- a/crates/core/database/src/util/bridge/v0.rs +++ b/crates/core/database/src/util/bridge/v0.rs @@ -691,6 +691,7 @@ impl From for FieldsMember { crate::FieldsMember::CanReceive => FieldsMember::CanReceive, crate::FieldsMember::CanPublish => FieldsMember::CanPublish, crate::FieldsMember::JoinedAt => FieldsMember::JoinedAt, + crate::FieldsMember::VoiceChannel => FieldsMember::VoiceChannel, } } } @@ -705,6 +706,7 @@ impl From for crate::FieldsMember { FieldsMember::CanReceive => crate::FieldsMember::CanReceive, FieldsMember::CanPublish => crate::FieldsMember::CanPublish, FieldsMember::JoinedAt => crate::FieldsMember::JoinedAt, + FieldsMember::VoiceChannel => crate::FieldsMember::VoiceChannel, } } } diff --git a/crates/core/models/src/v0/server_members.rs b/crates/core/models/src/v0/server_members.rs index d1ccb058..41ec99ae 100644 --- a/crates/core/models/src/v0/server_members.rs +++ b/crates/core/models/src/v0/server_members.rs @@ -95,6 +95,7 @@ auto_derived!( CanReceive, CanPublish, JoinedAt, + VoiceChannel, } /// Member removal intention diff --git a/crates/delta/src/routes/servers/member_edit.rs b/crates/delta/src/routes/servers/member_edit.rs index 458c10ba..ecc1d919 100644 --- a/crates/delta/src/routes/servers/member_edit.rs +++ b/crates/delta/src/routes/servers/member_edit.rs @@ -98,13 +98,21 @@ pub async fn edit( permissions.throw_if_lacking_channel_permission(ChannelPermission::DeafenMembers)?; } - let new_voice_channel = if let Some(new_channel) = &data.voice_channel { + if data.voice_channel.is_some() && data.remove.contains(&FieldsMember::VoiceChannel) { + return Err(create_error!(InvalidOperation)) + } + + if data.voice_channel.is_some() || data.remove.contains(&FieldsMember::VoiceChannel) { if !voice_client.is_enabled() { return Err(create_error!(LiveKitUnavailable)); }; - permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveMembers)?; + if member.id.user != user.id { + permissions.throw_if_lacking_channel_permission(ChannelPermission::MoveMembers)?; + } + } + let new_voice_channel = if let Some(new_channel) = &data.voice_channel { // ensure the channel we are moving them to is in the server and is a voice channel let channel = Reference::from_unchecked(new_channel) @@ -116,6 +124,9 @@ pub async fn edit( Err(create_error!(UnknownChannel))? } + let channel_permissions = calculate_channel_permissions(&mut query.clone().channel(&channel)).await; + channel_permissions.throw_if_lacking_channel_permission(ChannelPermission::Connect)?; + if get_user_voice_channel_in_server(&target_user.id, &server.id) .await? .is_none() @@ -189,10 +200,8 @@ pub async fn edit( partial.avatar = Some(File::use_user_avatar(db, &avatar, &user.id, &user.id).await?); } - let remove_contains_voice = remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive); - member - .update(db, partial, remove.into_iter().map(Into::into).collect()) + .update(db, partial, remove.clone().into_iter().map(Into::into).collect()) .await?; if let Some(new_voice_channel) = new_voice_channel { @@ -234,7 +243,7 @@ pub async fn edit( .private(target_user.id.clone()) .await; }; - } else if can_publish.is_some() || can_receive.is_some() || remove_contains_voice { + } else if can_publish.is_some() || can_receive.is_some() || remove.contains(FieldsMember::CanPublish) || remove.contains(FieldsMember::CanReceive) { if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await? { let node = get_channel_node(&channel).await?.unwrap(); @@ -253,5 +262,14 @@ pub async fn edit( }; }; + if remove.contains(&FieldsMember::VoiceChannel) { + if let Some(channel) = get_user_voice_channel_in_server(&target_user.id, &server.id).await? + { + let node = get_channel_node(&channel).await?.unwrap(); + + voice_client.remove_user(&node, &user.id, &channel).await?; + }; + } + Ok(Json(member.into())) }