Compare commits

..

9 commits

Author SHA1 Message Date
Raphael
8c011d29fd
fix(command/moderation): adding the position 0 for ban
The serenity lib return None on .member_highest_role so adding an
unwrap_or(0)
2026-03-16 14:40:20 +01:00
Raphael
38228d8cee
fix(command/moderation): adding the position 0 for kick
The serenity lib return None on .member_highest_role so adding an
unwrap_or(0)
2026-03-16 14:40:17 +01:00
Raphael
05a1ff5c7b
feat(command/moderation): adding the ban command 2026-03-16 14:05:20 +01:00
Raphael
e7a7c0c578
style(command/moderation): adding the format_sanction_reason function 2026-03-16 14:05:05 +01:00
Raphael
83d0c0dace
feat(command/utils): adding the debug print to help 2026-03-16 14:04:23 +01:00
Raphael
3385bb3660
feat(utils/format): adding the format_sanction module 2026-03-16 14:03:54 +01:00
Raphael
e388ddfc8c
feat(command/gestion): adding the debug print to set 2026-03-16 14:03:39 +01:00
Raphael
d0bedf904c
feat(command/moderation): adding the debug print to clear 2026-03-16 14:03:15 +01:00
Raphael
4f3badf5a8
feat(command/moderation): adding the kick command
- This command is for kick user from the guild
2026-03-16 00:45:37 +01:00
6 changed files with 250 additions and 10 deletions

View file

@ -20,13 +20,13 @@ async fn set_picture(slashcmd: &Set, ctx: &Context, cmd: &CommandInteraction, db
_ => return Err(anyhow::anyhow!("Expected a subcommand")), _ => return Err(anyhow::anyhow!("Expected a subcommand")),
}; };
let url = inner_options let url: &str = inner_options
.iter() .iter()
.find(|opt| opt.name == "link") .find(|opt| opt.name == "link")
.ok_or_else(|| anyhow::anyhow!("Option 'link' not found"))? .ok_or_else(|| anyhow::anyhow!("Option 'link' not found"))?
.value .value
.as_str() .as_str()
.ok_or_else(|| anyhow::anyhow!("Option 'link' is not a string"))?; .ok_or_else(|| anyhow::anyhow!("Option 'link' is not a string"))?;
let attachment: CreateAttachment = CreateAttachment::url(&ctx.http, &url) let attachment: CreateAttachment = CreateAttachment::url(&ctx.http, &url)
.await?; .await?;
@ -85,7 +85,7 @@ impl SlashCommand for Set {
_database: &PgPool, _database: &PgPool,
_emoji: &EmojiConfig, _emoji: &EmojiConfig,
) -> Result<()> { ) -> Result<()> {
debug!("Set command called"); debug!("{} command called", self.name());
if !is_owner(_database, &command.user.id.to_string()).await? { if !is_owner(_database, &command.user.id.to_string()).await? {
let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new()
.content(format!("{} | This command is only for the owner", _emoji.answer.no)) .content(format!("{} | This command is only for the owner", _emoji.answer.no))

View file

@ -0,0 +1,117 @@
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<CreateCommandOption> = 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_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);
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) }
}

View file

@ -50,7 +50,7 @@ impl SlashCommand for Clear {
_database: &PgPool, _database: &PgPool,
_emoji: &EmojiConfig, _emoji: &EmojiConfig,
) -> Result<()> { ) -> Result<()> {
debug!("Clear command called"); debug!("{} command called", self.name());
let amount: u8 = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::Integer) let amount: u8 = command.data.options.iter().find(|opt | opt.kind() == CommandOptionType::Integer)
.unwrap().value.as_i64().expect("REASON") as u8; .unwrap().value.as_i64().expect("REASON") as u8;
let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new() let message: CreateInteractionResponseMessage = CreateInteractionResponseMessage::new()

View file

@ -0,0 +1,117 @@
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 Kick;
#[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<CreateCommandOption> = 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_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)
.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_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);
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) }
}

View file

@ -94,7 +94,7 @@ impl SlashCommand for Help {
_database: &PgPool, _database: &PgPool,
_emoji: &EmojiConfig, _emoji: &EmojiConfig,
) -> Result<()> { ) -> 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: GuildId = command.guild_id.ok_or(serenity::Error::Other("Commande non disponible en DM"))?;
let guild_id: String = guild.to_string(); let guild_id: String = guild.to_string();
let guild_db: Option<DbGuild> = guild::get(_database, &guild_id).await.map_err(|_e| serenity::Error::Other("Database error guild on help command"))?; let guild_db: Option<DbGuild> = guild::get(_database, &guild_id).await.map_err(|_e| serenity::Error::Other("Database error guild on help command"))?;

6
src/utils/format.rs Normal file
View file

@ -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)
}
}