From 4f3badf5a8045c6d70d1c77f64c8b673dbf52163 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 00:45:37 +0100 Subject: [PATCH 1/9] feat(command/moderation): adding the kick command - This command is for kick user from the guild --- src/commands/moderation/kick.rs | 114 ++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/commands/moderation/kick.rs diff --git a/src/commands/moderation/kick.rs b/src/commands/moderation/kick.rs new file mode 100644 index 0000000..3062144 --- /dev/null +++ b/src/commands/moderation/kick.rs @@ -0,0 +1,114 @@ +use crate::commands::{CommandCategory, CommandEntry, SlashCommand}; +use crate::config::EmojiConfig; + +use serenity::all::{ + CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponse, CreateInteractionResponseMessage, EditInteractionResponse, GetMessages, Guild, GuildId, InteractionContext, Member, Message, MessageId, Permissions, Role, User, UserId +}; +use sqlx::PgPool; +use tracing::{debug, info}; +use anyhow::Result; + +pub struct Kick; + +fn format_reason(reason: Option<&str>, executor: &str) -> String { + match reason { + Some(s) => format!("[TTY kick] {} by {}", s, executor), + None => format!("[TTY kick] by {}", executor) + } +} + +#[serenity::async_trait] +impl SlashCommand for Kick { + fn name(&self) -> &'static str { + "kick" + } + + fn description(&self) -> &'static str { + "Kick the user provided" + } + + fn category(&self) -> &'static CommandCategory { + &CommandCategory::Moderation + } + + fn register(&self) -> CreateCommand { + info!("\t✅ | {}", self.name()); + let mut options: Vec = Vec::new(); + + let target: CreateCommandOption = CreateCommandOption::new(CommandOptionType::User, "user", "The user to kick") + .required(true); + options.push(target); + + let reason: CreateCommandOption = CreateCommandOption::new(CommandOptionType::String, "reason", "The reason to kick this user") + .required(false); + options.push(reason); + + CreateCommand::new(self.name()) + .description(self.description()) + .default_member_permissions(Permissions::KICK_MEMBERS) + .set_options(options) + .contexts(vec![ + InteractionContext::Guild, + ]) + } + + async fn run( + &self, + ctx: &Context, + command: &CommandInteraction, + _database: &PgPool, + _emoji: &EmojiConfig, + ) -> Result<()> { + debug!("{} command called", self.name()); + let target_id: UserId = command.data.options.iter() + .find(|opt| opt.kind() == CommandOptionType::User) + .and_then(|opt| opt.value.as_user_id()) + .ok_or_else(|| anyhow::anyhow!("Aucun utilisateur spécifié"))?; + let target: User = command.data.resolved.users.get(&target_id) + .ok_or_else(|| anyhow::anyhow!("Utilisateur introuvable"))? + .clone(); + let reason_provided: Option<&str> = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::String).and_then(|opt| opt.value.as_str()); + let reason_formatted: String = format_reason(reason_provided, &command.user.name); + + let guild_id: GuildId = command.guild_id.ok_or(anyhow::anyhow!("Kick command executed in DM"))?; + let guild: Guild = ctx.cache.guild(guild_id) + .ok_or_else(|| anyhow::anyhow!("Guild not found in cache"))? + .clone(); + + let target_member: Member = guild_id.member(&ctx.http, target_id).await?; + let executor_member: Member = guild_id.member(&ctx.http, command.user.id).await?; + let bot_id: UserId = ctx.cache.current_user().id; + let bot_member: Member = guild_id.member(&ctx.http, bot_id).await?; + + if reason_formatted.len() > 512 { + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | Reason too long (> 512 char)", _emoji.answer.error)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + return Ok(()); + } + let target_role: &Role = guild.member_highest_role(&target_member).unwrap(); + let executor_role: &Role = guild.member_highest_role(&executor_member).unwrap(); + let bot_role: &Role = guild.member_highest_role(&bot_member).unwrap(); + if target_role > executor_role || target_role > bot_role || target_id == guild.owner_id { + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | You cannot kick this user because they are hierarchically above you", _emoji.answer.error)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + return Ok(()); + } + guild_id.kick_with_reason(&ctx.http, target.id, &reason_formatted).await?; + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | {} is now kick", _emoji.answer.yes, target.name)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + Ok(()) + } +} + +inventory::submit! { + CommandEntry { create: || Box::new(Kick) } +} From d0bedf904cfab7035c08d499575d7b878de2f018 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:03:15 +0100 Subject: [PATCH 2/9] feat(command/moderation): adding the debug print to clear --- src/commands/moderation/clear.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/moderation/clear.rs b/src/commands/moderation/clear.rs index 1fadbfd..b9ce744 100644 --- a/src/commands/moderation/clear.rs +++ b/src/commands/moderation/clear.rs @@ -50,7 +50,7 @@ impl SlashCommand for Clear { _database: &PgPool, _emoji: &EmojiConfig, ) -> Result<()> { - debug!("Clear command called"); + debug!("{} command called", self.name()); let amount: u8 = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::Integer) .unwrap().value.as_i64().expect("REASON") as u8; let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() From e388ddfc8cceba1ff7e4c5ae8c42c6d85fe3d078 Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:03:39 +0100 Subject: [PATCH 3/9] feat(command/gestion): adding the debug print to set --- src/commands/gestion/set.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/commands/gestion/set.rs b/src/commands/gestion/set.rs index 34cebc7..13a8eea 100644 --- a/src/commands/gestion/set.rs +++ b/src/commands/gestion/set.rs @@ -20,13 +20,13 @@ async fn set_picture(slashcmd: &Set, ctx: &Context, cmd: &CommandInteraction, db _ => return Err(anyhow::anyhow!("Expected a subcommand")), }; - let url = inner_options - .iter() - .find(|opt| opt.name == "link") - .ok_or_else(|| anyhow::anyhow!("Option 'link' not found"))? - .value - .as_str() - .ok_or_else(|| anyhow::anyhow!("Option 'link' is not a string"))?; + let url: &str = inner_options + .iter() + .find(|opt| opt.name == "link") + .ok_or_else(|| anyhow::anyhow!("Option 'link' not found"))? + .value + .as_str() + .ok_or_else(|| anyhow::anyhow!("Option 'link' is not a string"))?; let attachment: CreateAttachment = CreateAttachment::url(&ctx.http, &url) .await?; @@ -85,7 +85,7 @@ impl SlashCommand for Set { _database: &PgPool, _emoji: &EmojiConfig, ) -> Result<()> { - debug!("Set command called"); + debug!("{} command called", self.name()); if !is_owner(_database, &command.user.id.to_string()).await? { let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() .content(format!("{} | This command is only for the owner", _emoji.answer.no)) From 3385bb36602deab8fa5981659f6830955219d41c Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:03:54 +0100 Subject: [PATCH 4/9] feat(utils/format): adding the format_sanction module --- src/utils/format.rs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/utils/format.rs diff --git a/src/utils/format.rs b/src/utils/format.rs new file mode 100644 index 0000000..00ac718 --- /dev/null +++ b/src/utils/format.rs @@ -0,0 +1,6 @@ +pub fn format_sanction_reason(module_name:&str, reason: Option<&str>, executor: &str) -> String { + match reason { + Some(s) => format!("[TTY {}] {} by {}", module_name, s, executor), + None => format!("[TTY {}] by {}", module_name, executor) + } +} From 83d0c0dacede54506f6baf59a63333c3d09a6b1d Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:04:23 +0100 Subject: [PATCH 5/9] feat(command/utils): adding the debug print to help --- src/commands/utils/help.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/utils/help.rs b/src/commands/utils/help.rs index 3a2f8b1..95f85da 100644 --- a/src/commands/utils/help.rs +++ b/src/commands/utils/help.rs @@ -94,7 +94,7 @@ impl SlashCommand for Help { _database: &PgPool, _emoji: &EmojiConfig, ) -> Result<()> { - debug!("Help command called"); + debug!("{} command called", self.name()); let guild: GuildId = command.guild_id.ok_or(serenity::Error::Other("Commande non disponible en DM"))?; let guild_id: String = guild.to_string(); let guild_db: Option = guild::get(_database, &guild_id).await.map_err(|_e| serenity::Error::Other("Database error guild on help command"))?; From e7a7c0c578850f42726911e66efaec93e591dc6b Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:05:05 +0100 Subject: [PATCH 6/9] style(command/moderation): adding the format_sanction_reason function --- src/commands/moderation/kick.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/commands/moderation/kick.rs b/src/commands/moderation/kick.rs index 3062144..efc8942 100644 --- a/src/commands/moderation/kick.rs +++ b/src/commands/moderation/kick.rs @@ -1,5 +1,6 @@ use crate::commands::{CommandCategory, CommandEntry, SlashCommand}; use crate::config::EmojiConfig; +use crate::utils::format::format_sanction_reason; use serenity::all::{ CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponse, CreateInteractionResponseMessage, EditInteractionResponse, GetMessages, Guild, GuildId, InteractionContext, Member, Message, MessageId, Permissions, Role, User, UserId @@ -10,13 +11,6 @@ use anyhow::Result; pub struct Kick; -fn format_reason(reason: Option<&str>, executor: &str) -> String { - match reason { - Some(s) => format!("[TTY kick] {} by {}", s, executor), - None => format!("[TTY kick] by {}", executor) - } -} - #[serenity::async_trait] impl SlashCommand for Kick { fn name(&self) -> &'static str { @@ -68,7 +62,7 @@ impl SlashCommand for Kick { .ok_or_else(|| anyhow::anyhow!("Utilisateur introuvable"))? .clone(); let reason_provided: Option<&str> = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::String).and_then(|opt| opt.value.as_str()); - let reason_formatted: String = format_reason(reason_provided, &command.user.name); + let reason_formatted: String = format_sanction_reason("kick", reason_provided, &command.user.name); let guild_id: GuildId = command.guild_id.ok_or(anyhow::anyhow!("Kick command executed in DM"))?; let guild: Guild = ctx.cache.guild(guild_id) From 05a1ff5c7b91718f0be046d9f72b523297f5d6ae Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:05:20 +0100 Subject: [PATCH 7/9] feat(command/moderation): adding the ban command --- src/commands/moderation/ban.rs | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/commands/moderation/ban.rs diff --git a/src/commands/moderation/ban.rs b/src/commands/moderation/ban.rs new file mode 100644 index 0000000..408fd04 --- /dev/null +++ b/src/commands/moderation/ban.rs @@ -0,0 +1,108 @@ +use crate::commands::{CommandCategory, CommandEntry, SlashCommand}; +use crate::config::EmojiConfig; +use crate::utils::format::format_sanction_reason; + +use serenity::all::{ + CommandInteraction, CommandOptionType, Context, CreateCommand, CreateCommandOption, CreateInteractionResponse, CreateInteractionResponseMessage, EditInteractionResponse, GetMessages, Guild, GuildId, InteractionContext, Member, Message, MessageId, Permissions, Role, User, UserId +}; +use sqlx::PgPool; +use tracing::{debug, info}; +use anyhow::Result; + +pub struct Ban; + +#[serenity::async_trait] +impl SlashCommand for Ban { + fn name(&self) -> &'static str { + "ban" + } + + fn description(&self) -> &'static str { + "Ban the user provided" + } + + fn category(&self) -> &'static CommandCategory { + &CommandCategory::Moderation + } + + fn register(&self) -> CreateCommand { + info!("\t✅ | {}", self.name()); + let mut options: Vec = Vec::new(); + + let target: CreateCommandOption = CreateCommandOption::new(CommandOptionType::User, "user", "The user to ban") + .required(true); + options.push(target); + + let reason: CreateCommandOption = CreateCommandOption::new(CommandOptionType::String, "reason", "The reason to ban this user") + .required(false); + options.push(reason); + + CreateCommand::new(self.name()) + .description(self.description()) + .default_member_permissions(Permissions::BAN_MEMBERS) + .set_options(options) + .contexts(vec![ + InteractionContext::Guild, + ]) + } + + async fn run( + &self, + ctx: &Context, + command: &CommandInteraction, + _database: &PgPool, + _emoji: &EmojiConfig, + ) -> Result<()> { + debug!("{} command called", self.name()); + let target_id: UserId = command.data.options.iter() + .find(|opt| opt.kind() == CommandOptionType::User) + .and_then(|opt| opt.value.as_user_id()) + .ok_or_else(|| anyhow::anyhow!("Aucun utilisateur spécifié"))?; + let target: User = command.data.resolved.users.get(&target_id) + .ok_or_else(|| anyhow::anyhow!("Utilisateur introuvable"))? + .clone(); + let reason_provided: Option<&str> = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::String).and_then(|opt| opt.value.as_str()); + let reason_formatted: String = format_sanction_reason("ban", reason_provided, &command.user.name); + + let guild_id: GuildId = command.guild_id.ok_or(anyhow::anyhow!("Ban command executed in DM"))?; + let guild: Guild = ctx.cache.guild(guild_id) + .ok_or_else(|| anyhow::anyhow!("Guild not found in cache"))? + .clone(); + + let target_member: Member = guild_id.member(&ctx.http, target_id).await?; + let executor_member: Member = guild_id.member(&ctx.http, command.user.id).await?; + let bot_id: UserId = ctx.cache.current_user().id; + let bot_member: Member = guild_id.member(&ctx.http, bot_id).await?; + + if reason_formatted.len() > 512 { + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | Reason too long (> 512 char)", _emoji.answer.error)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + return Ok(()); + } + let target_role: &Role = guild.member_highest_role(&target_member).unwrap(); + let executor_role: &Role = guild.member_highest_role(&executor_member).unwrap(); + let bot_role: &Role = guild.member_highest_role(&bot_member).unwrap(); + if target_role > executor_role || target_role > bot_role || target_id == guild.owner_id { + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | You cannot ban this user because they are hierarchically above you", _emoji.answer.error)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + return Ok(()); + } + guild_id.ban_with_reason(&ctx.http, target.id, 0, &reason_formatted).await?; + let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() + .content(format!("{} | {} is now ban", _emoji.answer.yes, target.name)) + .ephemeral(true); + let response: CreateInteractionResponse = CreateInteractionResponse::Message(message); + command.create_response(&ctx.http, response).await?; + Ok(()) + } +} + +inventory::submit! { + CommandEntry { create: || Box::new(Ban) } +} From 38228d8cee00a9b1cbc5a64a2406bb042cc5496b Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:40:17 +0100 Subject: [PATCH 8/9] fix(command/moderation): adding the position 0 for kick The serenity lib return None on .member_highest_role so adding an unwrap_or(0) --- src/commands/moderation/kick.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/kick.rs b/src/commands/moderation/kick.rs index efc8942..b4390d9 100644 --- a/src/commands/moderation/kick.rs +++ b/src/commands/moderation/kick.rs @@ -82,10 +82,19 @@ impl SlashCommand for Kick { command.create_response(&ctx.http, response).await?; return Ok(()); } - let target_role: &Role = guild.member_highest_role(&target_member).unwrap(); - let executor_role: &Role = guild.member_highest_role(&executor_member).unwrap(); - let bot_role: &Role = guild.member_highest_role(&bot_member).unwrap(); - if target_role > executor_role || target_role > bot_role || target_id == guild.owner_id { + let target_role_pos: u16 = guild + .member_highest_role(&target_member) + .map(|r| r.position) + .unwrap_or(0); + let executor_role_pos: u16 = guild + .member_highest_role(&executor_member) + .map(|r| r.position) + .unwrap_or(0); + let bot_role_pos: u16 = guild + .member_highest_role(&bot_member) + .map(|r| r.position) + .unwrap_or(0); + if target_role_pos > executor_role_pos || target_role_pos > bot_role_pos || target_id == guild.owner_id { let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() .content(format!("{} | You cannot kick this user because they are hierarchically above you", _emoji.answer.error)) .ephemeral(true); From 8c011d29fd906ef9670cac4db8ccfd2a17e0d34c Mon Sep 17 00:00:00 2001 From: Raphael Date: Mon, 16 Mar 2026 14:40:20 +0100 Subject: [PATCH 9/9] fix(command/moderation): adding the position 0 for ban The serenity lib return None on .member_highest_role so adding an unwrap_or(0) --- src/commands/moderation/ban.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/commands/moderation/ban.rs b/src/commands/moderation/ban.rs index 408fd04..1c53f8b 100644 --- a/src/commands/moderation/ban.rs +++ b/src/commands/moderation/ban.rs @@ -82,10 +82,19 @@ impl SlashCommand for Ban { command.create_response(&ctx.http, response).await?; return Ok(()); } - let target_role: &Role = guild.member_highest_role(&target_member).unwrap(); - let executor_role: &Role = guild.member_highest_role(&executor_member).unwrap(); - let bot_role: &Role = guild.member_highest_role(&bot_member).unwrap(); - if target_role > executor_role || target_role > bot_role || target_id == guild.owner_id { + let target_role_pos: u16 = guild + .member_highest_role(&target_member) + .map(|r| r.position) + .unwrap_or(0); + let executor_role_pos: u16 = guild + .member_highest_role(&executor_member) + .map(|r| r.position) + .unwrap_or(0); + let bot_role_pos: u16 = guild + .member_highest_role(&bot_member) + .map(|r| r.position) + .unwrap_or(0); + if target_role_pos > executor_role_pos || target_role_pos > bot_role_pos || target_id == guild.owner_id { let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() .content(format!("{} | You cannot ban this user because they are hierarchically above you", _emoji.answer.error)) .ephemeral(true);