diff --git a/src/commands/admin/addrole.rs b/src/commands/admin/addrole.rs index 6347ae1..19deaa7 100644 --- a/src/commands/admin/addrole.rs +++ b/src/commands/admin/addrole.rs @@ -1,9 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{parse_role, send_embed, theme_color}; + pub async fn handle_addrole(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_add_del_role(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 2 { + return; + } + + let Some(target) = parse_user_id(args[0]) else { + return; + }; + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + let Some(role) = parse_role(&guild, args[1]) else { + return; + }; + + let done = if let Ok(member) = guild_id.member(&ctx.http, target).await { + member.add_role(&ctx.http, role.id).await.is_ok() + } else { + false + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("AddRole") + .description(if done { + format!("Role <@&{}> ajoute a <@{}>.", role.id.get(), target.get()) + } else { + "Echec de modification du role.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct AddroleCommand; @@ -12,14 +50,12 @@ pub static COMMAND_DESCRIPTOR: AddroleCommand = AddroleCommand; impl crate::commands::command_contract::CommandSpec for AddroleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "addrole", - command: "addrole", + name: "addrole", category: "admin", params: "<@membre/ID[,..]> <@role/ID>", summary: "Ajoute un role", description: "Ajoute un role a un ou plusieurs membres.", examples: &["+addrole @User @Membre"], - alias_source_key: "addrole", default_aliases: &["ar"], default_permission: 8, } diff --git a/src/commands/admin/autobackup.rs b/src/commands/admin/autobackup.rs index 7c00f0c..268797c 100644 --- a/src/commands/admin/autobackup.rs +++ b/src/commands/admin/autobackup.rs @@ -1,10 +1,70 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; +use crate::db::DbPoolKey; + +async fn pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} pub async fn handle_autobackup(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_autobackup(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(kind_raw) = args.first() else { + return; + }; + let Some(days_raw) = args.get(1) else { + return; + }; + + let Some(kind) = advanced_tools::backup_kind_from_input(kind_raw) else { + return; + }; + + let Ok(days) = days_raw.parse::() else { + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + + let bot_id = ctx.cache.current_user().id; + let _ = sqlx::query( + r#" + INSERT INTO bot_autobackups (bot_id, guild_id, kind, interval_days, next_run_at) + VALUES ($1, $2, $3, $4, NOW() + make_interval(days => $4)) + ON CONFLICT (bot_id, guild_id, kind) + DO UPDATE SET interval_days = EXCLUDED.interval_days, + next_run_at = NOW() + make_interval(days => EXCLUDED.interval_days); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(kind) + .bind(days.max(1)) + .execute(&pool) + .await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("AutoBackup") + .description(format!( + "Auto-backup `{}` configuree toutes les {} jours.", + kind, + days.max(1) + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct AutoBackupCommand; @@ -13,14 +73,12 @@ pub static COMMAND_DESCRIPTOR: AutoBackupCommand = AutoBackupCommand; impl crate::commands::command_contract::CommandSpec for AutoBackupCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "autobackup", - command: "autobackup", + name: "autobackup", category: "admin", params: " ", summary: "Configure les backups automatiques", description: "Definit l'intervalle en jours des backups automatiques.", examples: &["+autobackup serveur 3", "+autobackup emoji 7"], - alias_source_key: "autobackup", default_aliases: &["abkp"], default_permission: 8, } diff --git a/src/commands/admin/autoconfiglog.rs b/src/commands/admin/autoconfiglog.rs index e5a8082..5555f88 100644 --- a/src/commands/admin/autoconfiglog.rs +++ b/src/commands/admin/autoconfiglog.rs @@ -1,9 +1,50 @@ -use crate::commands::logs_service; +use serenity::builder::{CreateChannel, CreateEmbed}; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::set_log_channel; + +const LOG_TYPES: &[(&str, &str)] = &[ + ("moderation", "modlog"), + ("message", "messagelog"), + ("voice", "voicelog"), + ("boost", "boostlog"), + ("role", "rolelog"), + ("raid", "raidlog"), + ("channel", "channellog"), +]; + pub async fn handle_autoconfiglog(ctx: &Context, msg: &Message) { - logs_service::handle_autoconfiglog(ctx, msg).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let mut created = Vec::new(); + for (log_type, cmd) in LOG_TYPES { + let name = format!("{}-logs", cmd.replace("log", "")); + if let Ok(channel) = guild_id + .create_channel(&ctx.http, CreateChannel::new(name).kind(ChannelType::Text)) + .await + { + set_log_channel(ctx, guild_id, log_type, Some(channel.id), true).await; + created.push(format!("{} -> <#{}>", log_type, channel.id.get())); + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("AutoConfigLog") + .description(if created.is_empty() { + "Aucun salon cree.".to_string() + } else { + created.join("\n") + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct AutoconfiglogCommand; @@ -12,14 +53,12 @@ pub static COMMAND_DESCRIPTOR: AutoconfiglogCommand = AutoconfiglogCommand; impl crate::commands::command_contract::CommandSpec for AutoconfiglogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "autoconfiglog", - command: "autoconfiglog", + name: "autoconfiglog", category: "admin", params: "aucun", summary: "Cree tous les salons de logs", description: "Cree automatiquement les salons de logs et les configure.", examples: &["+autoconfiglog"], - alias_source_key: "autoconfiglog", default_aliases: &["acl"], default_permission: 8, } diff --git a/src/commands/admin/autopublish.rs b/src/commands/admin/autopublish.rs index 38041fd..5f743d3 100644 --- a/src/commands/admin/autopublish.rs +++ b/src/commands/admin/autopublish.rs @@ -110,3 +110,25 @@ pub async fn handle_autopublish(ctx: &Context, msg: &Message, args: &[&str]) { send_embed(ctx, msg, embed).await; } + +pub struct AutopublishCommand; +pub static COMMAND_DESCRIPTOR: AutopublishCommand = AutopublishCommand; + +impl crate::commands::command_contract::CommandSpec for AutopublishCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "autopublish", + category: "admin", + params: "on|off [#canal]", + summary: "Configure lautopublish", + description: "Affiche, active ou desactive la publication automatique des annonces.", + examples: &[ + "+autopublish", + "+autopublish on #annonces", + "+help autopublish", + ], + default_aliases: &["apb"], + default_permission: 8, + } + } +} diff --git a/src/commands/admin/autoreact.rs b/src/commands/admin/autoreact.rs index 2577884..678882a 100644 --- a/src/commands/admin/autoreact.rs +++ b/src/commands/admin/autoreact.rs @@ -1,10 +1,140 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::db::DbPoolKey; + +fn owned_component_id(action: &str, owner_id: UserId) -> String { + format!("{}:{}", action, owner_id.get()) +} + +async fn pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} pub async fn handle_autoreact(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_autoreact(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("AutoReact") + .description("Utilise les boutons pour ajouter/supprimer/lister via UI.") + .color(theme_color(ctx).await); + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(owned_component_id("adv:autoreact:add_modal", msg.author.id)) + .label("Ajouter") + .style(ButtonStyle::Success), + CreateButton::new(owned_component_id("adv:autoreact:del_modal", msg.author.id)) + .label("Supprimer") + .style(ButtonStyle::Danger), + CreateButton::new(owned_component_id("adv:autoreact:list", msg.author.id)) + .label("Lister") + .style(ButtonStyle::Primary), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + + let bot_id = ctx.cache.current_user().id; + + if action == "list" { + let rows = sqlx::query_as::<_, (i64, String)>( + r#" + SELECT channel_id, emoji + FROM bot_autoreacts + WHERE bot_id = $1 AND guild_id = $2 + ORDER BY channel_id ASC, emoji ASC; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_all(&pool) + .await + .unwrap_or_default(); + + let desc = if rows.is_empty() { + "Aucun autoreact configure.".to_string() + } else { + rows.into_iter() + .map(|(channel_id, emoji)| format!("- <#{}> -> {}", channel_id, emoji)) + .collect::>() + .join("\n") + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("AutoReact") + .description(desc) + .color(theme_color(ctx).await), + ) + .await; + return; + } + + if args.len() < 3 { + return; + } + + let Some(channel_id) = parse_channel_id(args[1]) else { + return; + }; + let emoji = args[2]; + + if action == "add" { + let _ = sqlx::query( + r#" + INSERT INTO bot_autoreacts (bot_id, guild_id, channel_id, emoji) + VALUES ($1, $2, $3, $4) + ON CONFLICT (bot_id, guild_id, channel_id, emoji) DO NOTHING; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel_id.get() as i64) + .bind(emoji) + .execute(&pool) + .await; + } else if action == "del" { + let _ = sqlx::query( + r#" + DELETE FROM bot_autoreacts + WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3 AND emoji = $4; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel_id.get() as i64) + .bind(emoji) + .execute(&pool) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("AutoReact") + .description("Configuration mise a jour.") + .color(theme_color(ctx).await), + ) + .await; } pub struct AutoReactCommand; @@ -13,14 +143,12 @@ pub static COMMAND_DESCRIPTOR: AutoReactCommand = AutoReactCommand; impl crate::commands::command_contract::CommandSpec for AutoReactCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "autoreact", - command: "autoreact", + name: "autoreact", category: "admin", params: " | list", summary: "Configure les reactions automatiques", description: "Ajoute, retire et liste les reactions automatiquement appliquees aux messages d'un salon.", examples: &["+autoreact add #general 😀", "+autoreact list"], - alias_source_key: "autoreact", default_aliases: &["ar", "reactauto"], default_permission: 8, } diff --git a/src/commands/admin/backup.rs b/src/commands/admin/backup.rs index c4a898a..92fb5b8 100644 --- a/src/commands/admin/backup.rs +++ b/src/commands/admin/backup.rs @@ -1,10 +1,160 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_backup(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_backup(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Utilisation: +backup | list/delete/load") + .color(0xED4245), + ) + .await; + return; + } + + let mut action = "create"; + let mut index = 0usize; + if let Some(first) = args.first() { + match first.to_lowercase().as_str() { + "list" | "ls" => { + action = "list"; + index = 1; + } + "delete" | "del" | "rm" => { + action = "delete"; + index = 1; + } + "load" => { + action = "load"; + index = 1; + } + _ => {} + } + } + + let Some(kind_raw) = args.get(index) else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Type invalide: utilise serveur ou emoji.") + .color(0xED4245), + ) + .await; + return; + }; + + let Some(kind) = advanced_tools::backup_kind_from_input(kind_raw) else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Type invalide: utilise serveur ou emoji.") + .color(0xED4245), + ) + .await; + return; + }; + + match action { + "list" => { + advanced_tools::backup_list(ctx, msg, guild_id, kind).await; + } + "delete" => { + let Some(name) = args + .get(index + 1) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Tu dois preciser un nom de backup.") + .color(0xED4245), + ) + .await; + return; + }; + advanced_tools::backup_delete(ctx, msg, guild_id, kind, name).await; + } + "load" => { + let Some(name) = args + .get(index + 1) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Tu dois preciser un nom de backup.") + .color(0xED4245), + ) + .await; + return; + }; + advanced_tools::backup_load(ctx, msg, guild_id, kind, name).await; + } + _ => { + let Some(name) = args + .get(index + 1) + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description("Tu dois preciser un nom de backup.") + .color(0xED4245), + ) + .await; + return; + }; + + match advanced_tools::backup_create(ctx, guild_id, kind, name).await { + Ok(()) => { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description(format!("Backup `{}` de type `{}` creee.", name, kind)) + .color(theme_color(ctx).await), + ) + .await; + } + Err(err) => { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Backup") + .description(format!("Erreur: {}", err)) + .color(0xED4245), + ) + .await; + } + } + } + } } pub struct BackupCommand; @@ -13,8 +163,7 @@ pub static COMMAND_DESCRIPTOR: BackupCommand = BackupCommand; impl crate::commands::command_contract::CommandSpec for BackupCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "backup", - command: "backup", + name: "backup", category: "admin", params: " | list/delete/load", summary: "Gere les backups serveur et emojis", @@ -24,7 +173,6 @@ impl crate::commands::command_contract::CommandSpec for BackupCommand { "+backup list serveur", "+backup load emoji nightly", ], - alias_source_key: "backup", default_aliases: &["bkp"], default_permission: 8, } diff --git a/src/commands/admin/ban.rs b/src/commands/admin/ban.rs index 6a45a93..bc5dea3 100644 --- a/src/commands/admin/ban.rs +++ b/src/commands/admin/ban.rs @@ -1,22 +1,72 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{add_sanction, parse_targets}; + pub async fn handle_ban(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_ban(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let reason = if args.len() > 1 { + args[1..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let mut done = 0usize; + for uid in &targets { + if guild_id + .ban_with_reason(&ctx.http, *uid, 0, &reason) + .await + .is_ok() + { + done += 1; + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "ban", + &reason, + None, + None, + ) + .await; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Ban") + .description(format!("{} membre(s) banni(s).", done)) + .color(theme_color(ctx).await), + ) + .await; } pub struct BanCommand; pub static COMMAND_DESCRIPTOR: BanCommand = BanCommand; impl crate::commands::command_contract::CommandSpec for BanCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "ban", - command: "ban", + name: "ban", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Bannit un membre", description: "Ban un ou plusieurs membres.", examples: &["+ban @User"], - alias_source_key: "ban", default_aliases: &["b"], default_permission: 8, } diff --git a/src/commands/admin/banlist.rs b/src/commands/admin/banlist.rs index fc1f71c..98ff375 100644 --- a/src/commands/admin/banlist.rs +++ b/src/commands/admin/banlist.rs @@ -1,22 +1,48 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; + pub async fn handle_banlist(ctx: &Context, msg: &Message) { - moderation_tools::handle_banlist(ctx, msg).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let bans = guild_id + .bans(&ctx.http, None, None) + .await + .unwrap_or_default(); + let desc = if bans.is_empty() { + "Aucun ban en cours.".to_string() + } else { + bans.into_iter() + .map(|ban| format!("- <@{}> ({})", ban.user.id.get(), ban.user.tag())) + .collect::>() + .join("\n") + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BanList") + .description(desc) + .color(theme_color(ctx).await), + ) + .await; } pub struct BanlistCommand; pub static COMMAND_DESCRIPTOR: BanlistCommand = BanlistCommand; impl crate::commands::command_contract::CommandSpec for BanlistCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "banlist", - command: "banlist", + name: "banlist", category: "admin", params: "aucun", summary: "Liste les bans", description: "Affiche la liste des bannissements en cours.", examples: &["+banlist"], - alias_source_key: "banlist", default_aliases: &["bls"], default_permission: 8, } diff --git a/src/commands/admin/bl.rs b/src/commands/admin/bl.rs index 72a697a..30bd9d2 100644 --- a/src/commands/admin/bl.rs +++ b/src/commands/admin/bl.rs @@ -78,14 +78,12 @@ pub static COMMAND_DESCRIPTOR: BlCommand = BlCommand; impl crate::commands::command_contract::CommandSpec for BlCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "bl", - command: "bl", + name: "bl", category: "admin", params: "[<@membre/ID> [raison...]]", summary: "Gere la blacklist globale", description: "Affiche la blacklist ou ajoute un utilisateur a la blacklist globale du bot.", examples: &["+bl", "+help bl"], - alias_source_key: "bl", default_aliases: &["bls"], default_permission: 9, } diff --git a/src/commands/admin/blinfo.rs b/src/commands/admin/blinfo.rs index 1a53503..5f0314f 100644 --- a/src/commands/admin/blinfo.rs +++ b/src/commands/admin/blinfo.rs @@ -83,14 +83,12 @@ pub static COMMAND_DESCRIPTOR: BlinfoCommand = BlinfoCommand; impl crate::commands::command_contract::CommandSpec for BlinfoCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "blinfo", - command: "blinfo", + name: "blinfo", category: "admin", params: "<@membre/ID>", summary: "Affiche les details blacklist", description: "Affiche les details de blacklist pour un utilisateur donne.", examples: &["+blinfo", "+bo", "+help blinfo"], - alias_source_key: "blinfo", default_aliases: &["bli"], default_permission: 9, } diff --git a/src/commands/admin/boostembed.rs b/src/commands/admin/boostembed.rs index 1d6283e..bdd8e78 100644 --- a/src/commands/admin/boostembed.rs +++ b/src/commands/admin/boostembed.rs @@ -119,7 +119,11 @@ async fn set_boost_log_channel( .await; } -async fn read_settings(pool: &sqlx::PgPool, bot_id: UserId, guild_id: GuildId) -> BoostEmbedSettings { +async fn read_settings( + pool: &sqlx::PgPool, + bot_id: UserId, + guild_id: GuildId, +) -> BoostEmbedSettings { let row = sqlx::query_as::<_, (bool, Option, Option, Option)>( r#" SELECT enabled, title, description, color @@ -235,20 +239,32 @@ fn settings_components(owner_id: UserId, settings: &BoostEmbedSettings) -> Vec bool { - if !modal.data.custom_id.starts_with(&format!("{}:modal:", BOOSTEMBED_MENU)) { + if !modal + .data + .custom_id + .starts_with(&format!("{}:modal:", BOOSTEMBED_MENU)) + { return false; } @@ -641,14 +677,12 @@ pub static COMMAND_DESCRIPTOR: BoostembedCommand = BoostembedCommand; impl crate::commands::command_contract::CommandSpec for BoostembedCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "boostembed", - command: "boostembed", + name: "boostembed", category: "admin", params: "[on|off|test|settings]", summary: "Configure l embed boost avec panneau interactif", description: "Ouvre un panneau avec composants pour paramétrer l'embed boost et le salon où il est envoyé.", examples: &["+boostembed", "+boostembed settings", "+boostembed test"], - alias_source_key: "boostembed", default_aliases: &["bembed"], default_permission: 8, } diff --git a/src/commands/admin/boostlog.rs b/src/commands/admin/boostlog.rs index aa070d1..7134d83 100644 --- a/src/commands/admin/boostlog.rs +++ b/src/commands/admin/boostlog.rs @@ -1,9 +1,55 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_boostlog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_log_toggle(ctx, msg, args, "boost", "BoostLog").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("BoostLog") + .description("Usage: +boostlog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + match action.as_str() { + "on" => { + let channel = parse_target_channel(msg, args, 1); + set_log_channel(ctx, guild_id, "boost", channel, true).await; + let embed = CreateEmbed::new() + .title("BoostLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + "off" => { + set_log_channel(ctx, guild_id, "boost", None, false).await; + let embed = CreateEmbed::new() + .title("BoostLog") + .description("Desactive.") + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + _ => { + let embed = CreateEmbed::new() + .title("BoostLog") + .description("Usage: +boostlog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + } + } } pub struct BoostlogCommand; @@ -12,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: BoostlogCommand = BoostlogCommand; impl crate::commands::command_contract::CommandSpec for BoostlogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "boostlog", - command: "boostlog", + name: "boostlog", category: "admin", params: "", summary: "Active les logs de boosts", description: "Active ou desactive les logs de boosts.", examples: &["+boostlog on #logs", "+boostlog off"], - alias_source_key: "boostlog", default_aliases: &["blog"], default_permission: 8, } diff --git a/src/commands/admin/bringall.rs b/src/commands/admin/bringall.rs index 8e0f7c4..6ef60d9 100644 --- a/src/commands/admin/bringall.rs +++ b/src/commands/admin/bringall.rs @@ -1,10 +1,62 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; pub async fn handle_bringall(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_bringall(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let target_channel = if let Some(raw) = args.first() { + parse_channel_id(raw) + } else { + let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { + return; + }; + guild + .voice_states + .get(&msg.author.id) + .and_then(|v| v.channel_id) + }; + + let Some(target_channel) = target_channel else { + return; + }; + + let user_ids = { + let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { + return; + }; + + guild + .voice_states + .iter() + .filter_map(|(uid, state)| state.channel_id.map(|_| *uid)) + .collect::>() + }; + + let mut moved = 0usize; + for user_id in user_ids { + if guild_id + .move_member(&ctx.http, user_id, target_channel) + .await + .is_ok() + { + moved += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BringAll") + .description(format!("{} membres déplacés.", moved)) + .color(theme_color(ctx).await), + ) + .await; } pub struct BringAllCommand; @@ -13,14 +65,12 @@ pub static COMMAND_DESCRIPTOR: BringAllCommand = BringAllCommand; impl crate::commands::command_contract::CommandSpec for BringAllCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "bringall", - command: "bringall", + name: "bringall", category: "admin", params: "[salon_vocal_destination]", summary: "Rassemble tous les vocaux", description: "Deplace tous les membres actuellement en vocal vers un salon cible.", examples: &["+bringall #Event", "+bringall"], - alias_source_key: "bringall", default_aliases: &["ball", "vbring"], default_permission: 8, } diff --git a/src/commands/admin/button.rs b/src/commands/admin/button.rs index 3362e2d..c1e0a77 100644 --- a/src/commands/admin/button.rs +++ b/src/commands/admin/button.rs @@ -1,10 +1,40 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_button(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_button(ctx, msg, args).await; + if args.len() < 2 { + return; + } + + let action = args[0].to_lowercase(); + let link = args[1]; + + if action == "add" { + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new() + .content("Bouton personnalisé") + .components(vec![CreateActionRow::Buttons(vec![ + CreateButton::new_link(link).label("Ouvrir"), + ])]), + ) + .await; + } else if action == "del" { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Button") + .description("Suppression: supprime simplement le message contenant le bouton.") + .color(theme_color(ctx).await), + ) + .await; + } } pub struct ButtonCommand; @@ -13,8 +43,7 @@ pub static COMMAND_DESCRIPTOR: ButtonCommand = ButtonCommand; impl crate::commands::command_contract::CommandSpec for ButtonCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "button", - command: "button", + name: "button", category: "admin", params: " ", summary: "Gere des boutons decoratifs", @@ -23,7 +52,6 @@ impl crate::commands::command_contract::CommandSpec for ButtonCommand { "+button add https://example.com", "+button del https://example.com", ], - alias_source_key: "button", default_aliases: &["btn"], default_permission: 8, } diff --git a/src/commands/admin/claim.rs b/src/commands/admin/claim.rs index 2ff6599..273b99f 100644 --- a/src/commands/admin/claim.rs +++ b/src/commands/admin/claim.rs @@ -67,3 +67,21 @@ pub async fn handle_claim(ctx: &Context, msg: &Message, _args: &[&str]) { ) .await; } + +pub struct ClaimCommand; +pub static COMMAND_DESCRIPTOR: ClaimCommand = ClaimCommand; + +impl crate::commands::command_contract::CommandSpec for ClaimCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "claim", + category: "admin", + params: "aucun", + summary: "Revendique un ticket", + description: "Assigne le ticket courant au moderateur qui execute la commande.", + examples: &["+claim", "+help claim"], + default_aliases: &[], + default_permission: 2, + } + } +} diff --git a/src/commands/admin/cleanup.rs b/src/commands/admin/cleanup.rs index 5d5d534..a823061 100644 --- a/src/commands/admin/cleanup.rs +++ b/src/commands/admin/cleanup.rs @@ -1,10 +1,55 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; pub async fn handle_cleanup(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_cleanup(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(channel_raw) = args.first() else { + return; + }; + let Some(channel_id) = parse_channel_id(channel_raw) else { + return; + }; + + let user_ids = { + let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { + return; + }; + + guild + .voice_states + .iter() + .filter_map(|(uid, state)| { + if state.channel_id == Some(channel_id) { + Some(*uid) + } else { + None + } + }) + .collect::>() + }; + + let mut kicked = 0usize; + for user_id in user_ids { + if guild_id.disconnect_member(&ctx.http, user_id).await.is_ok() { + kicked += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Cleanup") + .description(format!("{} utilisateurs déconnectés.", kicked)) + .color(theme_color(ctx).await), + ) + .await; } pub struct CleanupCommand; @@ -13,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: CleanupCommand = CleanupCommand; impl crate::commands::command_contract::CommandSpec for CleanupCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "cleanup", - command: "cleanup", + name: "cleanup", category: "admin", params: "", summary: "Vide un salon vocal", description: "Deconnecte tous les utilisateurs presents dans un salon vocal cible.", examples: &["+cleanup #General"], - alias_source_key: "cleanup", default_aliases: &["vclean", "vcleanup"], default_permission: 8, } diff --git a/src/commands/admin/clear_all_sanctions.rs b/src/commands/admin/clear_all_sanctions.rs index 1b47f21..33e3e70 100644 --- a/src/commands/admin/clear_all_sanctions.rs +++ b/src/commands/admin/clear_all_sanctions.rs @@ -1,10 +1,50 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::moderation_tools; +use crate::commands::common::{send_embed, theme_color}; +use crate::db::DbPoolKey; pub async fn handle_clear_all_sanctions(ctx: &Context, msg: &Message) { - moderation_tools::handle_clear_all_sanctions(ctx, msg).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + let Some(pool) = pool else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let removed = sqlx::query( + r#" + DELETE FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .execute(&pool) + .await + .ok() + .map(|r| r.rows_affected()) + .unwrap_or(0); + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Sanctions") + .description(format!( + "{} sanction(s) supprimée(s) sur le serveur.", + removed + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct ClearAllSanctionsCommand; @@ -13,14 +53,12 @@ pub static COMMAND_DESCRIPTOR: ClearAllSanctionsCommand = ClearAllSanctionsComma impl crate::commands::command_contract::CommandSpec for ClearAllSanctionsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_all_sanctions", - command: "clear all sanctions", + name: "clear_all_sanctions", category: "admin", params: "aucun", summary: "Supprime toutes les sanctions du serveur", description: "Efface toutes les sanctions de tous les membres du serveur.", examples: &["+clear all sanctions"], - alias_source_key: "clear_all_sanctions", default_aliases: &["casanctions"], default_permission: 8, } diff --git a/src/commands/admin/clear_bl.rs b/src/commands/admin/clear_bl.rs index 1ab89f5..0bb64b1 100644 --- a/src/commands/admin/clear_bl.rs +++ b/src/commands/admin/clear_bl.rs @@ -39,14 +39,12 @@ pub static COMMAND_DESCRIPTOR: ClearBlCommand = ClearBlCommand; impl crate::commands::command_contract::CommandSpec for ClearBlCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_bl", - command: "clear bl", + name: "clear_bl", category: "admin", params: "aucun", summary: "Vide la blacklist globale", description: "Supprime toutes les entrees de la blacklist globale.", examples: &["+clear bl", "+cl", "+help clear bl"], - alias_source_key: "clear_bl", default_aliases: &["cbl"], default_permission: 9, } diff --git a/src/commands/admin/clear_messages.rs b/src/commands/admin/clear_messages.rs index 35a06b9..b2380e6 100644 --- a/src/commands/admin/clear_messages.rs +++ b/src/commands/admin/clear_messages.rs @@ -1,10 +1,51 @@ +use serenity::builder::{CreateEmbed, GetMessages}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::moderation_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_clear_messages(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_clear_messages(ctx, msg, args).await; + let Ok(mut amount) = args.first().unwrap_or(&"0").parse::() else { + return; + }; + if amount == 0 { + return; + } + amount = amount.clamp(1, 100); + + let filter_user = args.get(1).and_then(|raw| parse_user_id(raw)); + + let mut deleted = 0usize; + if let Ok(messages) = msg + .channel_id + .messages(&ctx.http, GetMessages::new().limit(amount as u8 + 1)) + .await + { + for m in messages { + if m.id == msg.id { + continue; + } + if let Some(user_id) = filter_user { + if m.author.id != user_id { + continue; + } + } + if msg.channel_id.delete_message(&ctx.http, m.id).await.is_ok() { + deleted += 1; + } + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Clear") + .description(format!("{} message(s) supprime(s).", deleted)) + .color(theme_color(ctx).await), + ) + .await; } pub struct ClearMessagesCommand; @@ -13,14 +54,12 @@ pub static COMMAND_DESCRIPTOR: ClearMessagesCommand = ClearMessagesCommand; impl crate::commands::command_contract::CommandSpec for ClearMessagesCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_messages", - command: "clear", + name: "clear_messages", category: "admin", params: " [@membre/ID]", summary: "Supprime des messages dans le salon", description: "Supprime un nombre de messages, optionnellement filtres par membre.", examples: &["+clear 20", "+clear 20 @User"], - alias_source_key: "clear_messages", default_aliases: &["purge"], default_permission: 8, } diff --git a/src/commands/admin/clear_owners.rs b/src/commands/admin/clear_owners.rs index ceb6bff..a5d26d9 100644 --- a/src/commands/admin/clear_owners.rs +++ b/src/commands/admin/clear_owners.rs @@ -39,14 +39,12 @@ pub static COMMAND_DESCRIPTOR: ClearOwnersCommand = ClearOwnersCommand; impl crate::commands::command_contract::CommandSpec for ClearOwnersCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_owners", - command: "clear owners", + name: "clear_owners", category: "admin", params: "aucun", summary: "Vide la liste des owners", description: "Supprime tous les owners supplementaires en base de donnees.", examples: &["+clear owners", "+cs", "+help clear owners"], - alias_source_key: "clear_owners", default_aliases: &["cro"], default_permission: 9, } diff --git a/src/commands/admin/clear_sanctions.rs b/src/commands/admin/clear_sanctions.rs index 4793c0a..6187016 100644 --- a/src/commands/admin/clear_sanctions.rs +++ b/src/commands/admin/clear_sanctions.rs @@ -1,10 +1,60 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::moderation_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{send_embed, theme_color}; +use crate::db::DbPoolKey; pub async fn handle_clear_sanctions(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_clear_sanctions(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 2 { + return; + } + + let Some(target) = parse_user_id(args[1]) else { + return; + }; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + let Some(pool) = pool else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let removed = sqlx::query( + r#" + DELETE FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(target.get() as i64) + .execute(&pool) + .await + .ok() + .map(|r| r.rows_affected()) + .unwrap_or(0); + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Sanctions") + .description(format!( + "{} sanction(s) supprimée(s) pour <@{}>.", + removed, + target.get() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct ClearSanctionsCommand; @@ -13,14 +63,12 @@ pub static COMMAND_DESCRIPTOR: ClearSanctionsCommand = ClearSanctionsCommand; impl crate::commands::command_contract::CommandSpec for ClearSanctionsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_sanctions", - command: "clear sanctions", + name: "clear_sanctions", category: "admin", params: "<@membre/ID>", summary: "Supprime toutes les sanctions d un membre", description: "Efface completement les sanctions d un membre cible.", examples: &["+clear sanctions @User"], - alias_source_key: "clear_sanctions", default_aliases: &["csanctions"], default_permission: 8, } diff --git a/src/commands/admin/close.rs b/src/commands/admin/close.rs index cebc6ba..840b4e2 100644 --- a/src/commands/admin/close.rs +++ b/src/commands/admin/close.rs @@ -74,3 +74,21 @@ pub async fn handle_close(ctx: &Context, msg: &Message, args: &[&str]) { send_embed(ctx, msg, embed).await; } + +pub struct CloseCommand; +pub static COMMAND_DESCRIPTOR: CloseCommand = CloseCommand; + +impl crate::commands::command_contract::CommandSpec for CloseCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "close", + category: "admin", + params: "[raison...]", + summary: "Ferme un ticket", + description: "Ferme le ticket courant et enregistre optionnellement une raison.", + examples: &["+close", "+close Raison", "+help close"], + default_aliases: &[], + default_permission: 2, + } + } +} diff --git a/src/commands/admin/cmute.rs b/src/commands/admin/cmute.rs index 8b96536..4402667 100644 --- a/src/commands/admin/cmute.rs +++ b/src/commands/admin/cmute.rs @@ -1,22 +1,68 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{ + add_sanction, channel_mute_users, parse_targets, +}; + pub async fn handle_cmute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_cmute(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let reason = if args.len() > 1 { + args[1..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let affected = channel_mute_users(ctx, msg.channel_id, &targets, true).await; + + for uid in &targets { + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "cmute", + &reason, + Some(msg.channel_id), + None, + ) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("CMute") + .description(format!("{} membre(s) cmute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct CmuteCommand; pub static COMMAND_DESCRIPTOR: CmuteCommand = CmuteCommand; impl crate::commands::command_contract::CommandSpec for CmuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "cmute", - command: "cmute", + name: "cmute", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Mute salon", description: "Mute un membre sur le salon courant.", examples: &["+cmute @User"], - alias_source_key: "cmute", default_aliases: &["cm"], default_permission: 8, } diff --git a/src/commands/admin/create.rs b/src/commands/admin/create.rs index 9c610f1..d2c6b77 100644 --- a/src/commands/admin/create.rs +++ b/src/commands/admin/create.rs @@ -1,10 +1,86 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; + +fn emoji_url_from_source(msg: &Message, source: &str) -> String { + if source.starts_with("http://") || source.starts_with("https://") { + return source.to_string(); + } + + if source.starts_with("<:") || source.starts_with("'); + let parts = cleaned.split(':').collect::>(); + if parts.len() == 3 { + let animated = parts[0] == "a"; + return format!( + "https://cdn.discordapp.com/emojis/{}.{}", + parts[2], + if animated { "gif" } else { "png" } + ); + } + } + + if let Some(att) = msg.attachments.first() { + return att.url.clone(); + } + + String::new() +} pub async fn handle_create(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_create(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.len() < 2 { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Create Emoji") + .description("Usage: +create ") + .color(0xED4245), + ) + .await; + return; + } + + let image_url = emoji_url_from_source(msg, args[0]); + if image_url.is_empty() { + return; + } + + let response = match reqwest::get(&image_url).await { + Ok(r) => r, + Err(_) => return, + }; + let bytes = match response.bytes().await { + Ok(b) => b, + Err(_) => return, + }; + + let data_uri = format!("data:image/png;base64,{}", { + use base64::Engine; + base64::engine::general_purpose::STANDARD.encode(bytes) + }); + + let result = guild_id.create_emoji(&ctx.http, args[1], &data_uri).await; + + let embed = if let Ok(emoji) = result { + CreateEmbed::new() + .title("Emoji") + .description(format!("Emoji cree: {}", emoji)) + .color(theme_color(ctx).await) + } else { + CreateEmbed::new() + .title("Emoji") + .description("Impossible de creer l'emoji.") + .color(0xED4245) + }; + + send_embed(ctx, msg, embed).await; } pub struct CreateCommand; @@ -13,8 +89,7 @@ pub static COMMAND_DESCRIPTOR: CreateCommand = CreateCommand; impl crate::commands::command_contract::CommandSpec for CreateCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "create", - command: "create", + name: "create", category: "admin", params: "[emoji/url] [nom]", summary: "Cree un emoji custom", @@ -23,7 +98,6 @@ impl crate::commands::command_contract::CommandSpec for CreateCommand { "+create <:blob:123456789012345678> blobcopy", "+create https://... logo", ], - alias_source_key: "create", default_aliases: &["mkemoji", "ce"], default_permission: 8, } diff --git a/src/commands/admin/del_sanction.rs b/src/commands/admin/del_sanction.rs index ff7ba99..8dcf864 100644 --- a/src/commands/admin/del_sanction.rs +++ b/src/commands/admin/del_sanction.rs @@ -1,10 +1,82 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::moderation_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{send_embed, theme_color}; +use crate::db::DbPoolKey; pub async fn handle_del_sanction(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_del_sanction(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 3 { + return; + } + + let Some(target) = parse_user_id(args[1]) else { + return; + }; + let Ok(index) = args[2].parse::() else { + return; + }; + if index == 0 { + return; + } + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + let Some(pool) = pool else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let rows = sqlx::query_as::<_, (i64,)>( + r#" + SELECT id + FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 + ORDER BY created_at DESC; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(target.get() as i64) + .fetch_all(&pool) + .await + .unwrap_or_default(); + + let Some((sanction_id,)) = rows.get(index - 1).copied() else { + return; + }; + + let _ = sqlx::query( + r#" + DELETE FROM bot_sanctions + WHERE id = $1 AND bot_id = $2 AND guild_id = $3; + "#, + ) + .bind(sanction_id) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .execute(&pool) + .await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Sanctions") + .description(format!( + "Sanction #{} supprimée pour <@{}>.", + sanction_id, + target.get() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct DelSanctionCommand; @@ -13,14 +85,12 @@ pub static COMMAND_DESCRIPTOR: DelSanctionCommand = DelSanctionCommand; impl crate::commands::command_contract::CommandSpec for DelSanctionCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "del_sanction", - command: "del sanction", + name: "del_sanction", category: "admin", params: "<@membre/ID> ", summary: "Supprime une sanction d un membre", description: "Supprime une sanction specifique dans l historique d un membre.", examples: &["+del sanction @User 1"], - alias_source_key: "del_sanction", default_aliases: &["delsanction"], default_permission: 8, } diff --git a/src/commands/admin/delrole.rs b/src/commands/admin/delrole.rs index 9f366aa..f841e5b 100644 --- a/src/commands/admin/delrole.rs +++ b/src/commands/admin/delrole.rs @@ -1,9 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{parse_role, send_embed, theme_color}; + pub async fn handle_delrole(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_add_del_role(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 2 { + return; + } + + let Some(target) = parse_user_id(args[0]) else { + return; + }; + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + let Some(role) = parse_role(&guild, args[1]) else { + return; + }; + + let done = if let Ok(member) = guild_id.member(&ctx.http, target).await { + member.remove_role(&ctx.http, role.id).await.is_ok() + } else { + false + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("DelRole") + .description(if done { + format!("Role <@&{}> retire a <@{}>.", role.id.get(), target.get()) + } else { + "Echec de modification du role.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct DelroleCommand; @@ -12,14 +50,12 @@ pub static COMMAND_DESCRIPTOR: DelroleCommand = DelroleCommand; impl crate::commands::command_contract::CommandSpec for DelroleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "delrole", - command: "delrole", + name: "delrole", category: "admin", params: "<@membre/ID[,..]> <@role/ID>", summary: "Retire un role", description: "Retire un role a un ou plusieurs membres.", examples: &["+delrole @User @Membre"], - alias_source_key: "delrole", default_aliases: &["dr"], default_permission: 8, } diff --git a/src/commands/admin/derank.rs b/src/commands/admin/derank.rs index 22da247..37ff03f 100644 --- a/src/commands/admin/derank.rs +++ b/src/commands/admin/derank.rs @@ -1,9 +1,48 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::parse_targets; + pub async fn handle_derank(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_derank(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let mut done = 0usize; + for uid in &targets { + if let Ok(member) = guild_id.member(&ctx.http, *uid).await { + let roles = member.roles.clone(); + let mut ok = true; + for role_id in roles { + if member.remove_role(&ctx.http, role_id).await.is_err() { + ok = false; + } + } + if ok { + done += 1; + } + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Derank") + .description(format!("{} membre(s) derank.", done)) + .color(theme_color(ctx).await), + ) + .await; } pub struct DerankCommand; @@ -12,14 +51,12 @@ pub static COMMAND_DESCRIPTOR: DerankCommand = DerankCommand; impl crate::commands::command_contract::CommandSpec for DerankCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "derank", - command: "derank", + name: "derank", category: "admin", params: "<@membre/ID[,..]>", summary: "Retire tous les roles", description: "Retire tous les roles gerables d un membre.", examples: &["+derank @User"], - alias_source_key: "derank", default_aliases: &["drk"], default_permission: 8, } diff --git a/src/commands/admin/embed.rs b/src/commands/admin/embed.rs index 7dfded9..dfbdd78 100644 --- a/src/commands/admin/embed.rs +++ b/src/commands/admin/embed.rs @@ -1,10 +1,65 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; + +fn owned_component_id(action: &str, owner_id: UserId) -> String { + format!("{}:{}", action, owner_id.get()) +} pub async fn handle_embed(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_embed_builder(ctx, msg, args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Embed") + .description("Utilise le bouton pour ouvrir le generateur d'embed.") + .color(theme_color(ctx).await); + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(owned_component_id("adv:embed:modal", msg.author.id)) + .label("Ouvrir le generateur") + .style(ButtonStyle::Primary), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; + return; + } + + let joined = args.join(" "); + let mut split = joined.splitn(2, '|').map(str::trim); + let title = split.next().unwrap_or_default(); + let description = split.next().unwrap_or_default(); + + if title.is_empty() || description.is_empty() { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Embed") + .description("Format attendu: +embed titre | description") + .color(0xED4245), + ) + .await; + return; + } + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed( + CreateEmbed::new() + .title(title) + .description(description) + .color(theme_color(ctx).await), + ), + ) + .await; } pub struct EmbedCommand; @@ -13,14 +68,12 @@ pub static COMMAND_DESCRIPTOR: EmbedCommand = EmbedCommand; impl crate::commands::command_contract::CommandSpec for EmbedCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "embed", - command: "embed", + name: "embed", category: "admin", params: "title | description (v1)", summary: "Ouvre le generateur d'embed", description: "Affiche un generateur d'embed interactif version rapide.", examples: &["+embed"], - alias_source_key: "embed", default_aliases: &["emb"], default_permission: 8, } diff --git a/src/commands/admin/end.rs b/src/commands/admin/end.rs index 87c767a..42c12fe 100644 --- a/src/commands/admin/end.rs +++ b/src/commands/admin/end.rs @@ -1,10 +1,99 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage, EditMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; + +fn owned_component_id(action: &str, owner_id: UserId) -> String { + format!("{}:{}", action, owner_id.get()) +} + +async fn handle_end_giveaway(ctx: &Context, msg: &Message, args: &[&str]) { + let message_id_raw = args + .get(1) + .or_else(|| args.first()) + .copied() + .unwrap_or_default(); + + let Ok(message_id) = message_id_raw.trim().parse::() else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("End") + .description("ID du message invalide.") + .color(0xED4245), + ) + .await; + return; + }; + + let result = msg + .channel_id + .edit_message( + &ctx.http, + MessageId::new(message_id), + EditMessage::new().content("🎉 Giveaway termine manuellement."), + ) + .await; + + let (description, color) = if result.is_ok() { + ("Giveaway termine.", theme_color(ctx).await) + } else { + ("Impossible de terminer ce giveaway.", 0xED4245) + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("End") + .description(description) + .color(color), + ) + .await; +} pub async fn handle_end(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_end(ctx, msg, args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("End") + .description("Utilise le bouton pour terminer un giveaway via modal.") + .color(theme_color(ctx).await); + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(owned_component_id("adv:giveaway:end_modal", msg.author.id)) + .label("Terminer un giveaway") + .style(ButtonStyle::Danger), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; + return; + } + + if args + .first() + .map(|v| v.eq_ignore_ascii_case("giveaway")) + .unwrap_or(false) + { + handle_end_giveaway(ctx, msg, args).await; + return; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("End") + .description("Usage: +end giveaway ") + .color(0xED4245), + ) + .await; } pub struct EndCommand; @@ -13,14 +102,12 @@ pub static COMMAND_DESCRIPTOR: EndCommand = EndCommand; impl crate::commands::command_contract::CommandSpec for EndCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "end", - command: "end", + name: "end", category: "admin", params: "giveaway ", summary: "Termine un giveaway par ID", description: "Permet de stopper instantanement un giveaway avec l'identifiant du message.", examples: &["+end giveaway 123456789012345678"], - alias_source_key: "end", default_aliases: &["gend"], default_permission: 0, } diff --git a/src/commands/admin/giveaway.rs b/src/commands/admin/giveaway.rs index 7e2243e..4cd9adf 100644 --- a/src/commands/admin/giveaway.rs +++ b/src/commands/admin/giveaway.rs @@ -1,10 +1,42 @@ +use serenity::builder::{ + CreateActionRow, CreateButton, CreateEmbed, CreateEmbedFooter, CreateMessage, +}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::theme_color; + +fn owned_component_id(action: &str, owner_id: UserId) -> String { + format!("{}:{}", action, owner_id.get()) +} pub async fn handle_giveaway(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_giveaway(ctx, msg, args).await; + let _ = args; + + let embed = CreateEmbed::new() + .title("Giveaway") + .description("Utilise les boutons pour creer ou terminer un giveaway via modal.") + .color(theme_color(ctx).await) + .footer(CreateEmbedFooter::new("UI avancee: Components + Modal")); + + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(owned_component_id("adv:giveaway:open_modal", msg.author.id)) + .label("Creer") + .emoji('🎉') + .style(ButtonStyle::Success), + CreateButton::new(owned_component_id("adv:giveaway:end_modal", msg.author.id)) + .label("Terminer") + .emoji('🛑') + .style(ButtonStyle::Danger), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; } pub struct GiveawayCommand; @@ -13,14 +45,12 @@ pub static COMMAND_DESCRIPTOR: GiveawayCommand = GiveawayCommand; impl crate::commands::command_contract::CommandSpec for GiveawayCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "giveaway", - command: "giveaway", + name: "giveaway", category: "admin", params: "aucun", summary: "Ouvre un menu de creation de giveaway", description: "Affiche une interface rapide pour initier un giveaway depuis le salon courant.", examples: &["+giveaway"], - alias_source_key: "giveaway", default_aliases: &["gstart", "gw"], default_permission: 8, } diff --git a/src/commands/admin/hide.rs b/src/commands/admin/hide.rs index 8af51de..4da8683 100644 --- a/src/commands/admin/hide.rs +++ b/src/commands/admin/hide.rs @@ -1,22 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_hide(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_hide_unhide(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let target = args + .first() + .and_then(|raw| parse_channel_id(raw)) + .unwrap_or(msg.channel_id); + + let ok = edit_channel_visibility(ctx, guild_id, target, None, Some(true)).await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Hide") + .description(if ok { + format!("Salon <#{}> mis a jour.", target.get()) + } else { + "Echec de mise a jour du salon.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct HideCommand; pub static COMMAND_DESCRIPTOR: HideCommand = HideCommand; impl crate::commands::command_contract::CommandSpec for HideCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "hide", - command: "hide", + name: "hide", category: "admin", params: "[salon]", summary: "Cache un salon", description: "Retire la visibilite d un salon.", examples: &["+hide", "+hide #general"], - alias_source_key: "hide", default_aliases: &["hd"], default_permission: 8, } diff --git a/src/commands/admin/hideall.rs b/src/commands/admin/hideall.rs index 2943d5c..a611007 100644 --- a/src/commands/admin/hideall.rs +++ b/src/commands/admin/hideall.rs @@ -1,9 +1,34 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_hideall(ctx: &Context, msg: &Message) { - moderation_tools::handle_hideall_unhideall(ctx, msg, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return; + }; + + let mut changed = 0usize; + for channel_id in channels.keys() { + if edit_channel_visibility(ctx, guild_id, *channel_id, None, Some(true)).await { + changed += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("HideAll") + .description(format!("{} salon(s) mis a jour.", changed)) + .color(theme_color(ctx).await), + ) + .await; } pub struct HideallCommand; @@ -12,14 +37,12 @@ pub static COMMAND_DESCRIPTOR: HideallCommand = HideallCommand; impl crate::commands::command_contract::CommandSpec for HideallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "hideall", - command: "hideall", + name: "hideall", category: "admin", params: "aucun", summary: "Cache tous les salons", description: "Retire la visibilite de tous les salons.", examples: &["+hideall"], - alias_source_key: "hideall", default_aliases: &["hda"], default_permission: 8, } diff --git a/src/commands/admin/invite.rs b/src/commands/admin/invite.rs index afc1da0..c9e1bbe 100644 --- a/src/commands/admin/invite.rs +++ b/src/commands/admin/invite.rs @@ -70,14 +70,12 @@ pub static COMMAND_DESCRIPTOR: InviteCommand = InviteCommand; impl crate::commands::command_contract::CommandSpec for InviteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "invite", - command: "invite", + name: "invite", category: "admin", params: "", summary: "Cree une invitation serveur", description: "Cree une invitation temporaire sur un serveur cible accessible par le bot.", examples: &["+invite", "+ie", "+help invite"], - alias_source_key: "invite", default_aliases: &["ivt"], default_permission: 8, } diff --git a/src/commands/admin/join.rs b/src/commands/admin/join.rs index b699deb..15b4694 100644 --- a/src/commands/admin/join.rs +++ b/src/commands/admin/join.rs @@ -1,9 +1,122 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, pool}; + pub async fn handle_join(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_join_leave_settings(ctx, msg, args, "join").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + if args.is_empty() || !args[0].eq_ignore_ascii_case("settings") { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("join settings") + .description("Usage: +join settings [on/off] [salon] [message...]") + .color(0xED4245), + ) + .await; + return; + } + + if args.len() == 1 { + let row = sqlx::query_as::<_, (bool, Option, Option)>( + r#" + SELECT enabled, channel_id, custom_message + FROM bot_join_leave_settings + WHERE bot_id = $1 AND guild_id = $2 AND kind = $3 + LIMIT 1; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind("join") + .fetch_optional(&pool) + .await + .ok() + .flatten(); + + let desc = if let Some((enabled, channel_id, custom_message)) = row { + format!( + "Etat: {}\nSalon: {}\nMessage: {}", + if enabled { "on" } else { "off" }, + channel_id + .map(|id| format!("<#{}>", id)) + .unwrap_or_else(|| "non defini".to_string()), + custom_message.unwrap_or_else(|| "(defaut)".to_string()) + ) + } else { + "Aucun reglage configure.".to_string() + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("join settings") + .description(desc) + .color(theme_color(ctx).await), + ) + .await; + return; + } + + let action = args[1].to_lowercase(); + let enabled = action == "on"; + let channel = if enabled { + parse_target_channel(msg, args, 2) + } else { + None + }; + let message_start = if enabled { 3 } else { 2 }; + let custom_message = if args.len() > message_start { + Some(args[message_start..].join(" ")) + } else { + None + }; + + let _ = sqlx::query( + r#" + INSERT INTO bot_join_leave_settings (bot_id, guild_id, kind, enabled, channel_id, custom_message) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (bot_id, guild_id, kind) + DO UPDATE SET enabled = EXCLUDED.enabled, channel_id = EXCLUDED.channel_id, + custom_message = EXCLUDED.custom_message, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind("join") + .bind(enabled) + .bind(channel.map(|c| c.get() as i64)) + .bind(custom_message) + .execute(&pool) + .await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("join settings") + .description(format!( + "{} {}", + if enabled { "Active" } else { "Desactive" }, + channel + .map(|c| format!("dans <#{}>", c.get())) + .unwrap_or_default() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct JoinCommand; @@ -12,8 +125,7 @@ pub static COMMAND_DESCRIPTOR: JoinCommand = JoinCommand; impl crate::commands::command_contract::CommandSpec for JoinCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "join", - command: "join", + name: "join", category: "admin", params: "settings [on/off] [salon] [message]", summary: "Parametre les actions de join", @@ -22,7 +134,6 @@ impl crate::commands::command_contract::CommandSpec for JoinCommand { "+join settings", "+join settings on #welcome Bienvenue {user}", ], - alias_source_key: "join", default_aliases: &["jset"], default_permission: 8, } diff --git a/src/commands/admin/kick.rs b/src/commands/admin/kick.rs index 61d5e0d..51656b2 100644 --- a/src/commands/admin/kick.rs +++ b/src/commands/admin/kick.rs @@ -1,22 +1,72 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{add_sanction, parse_targets}; + pub async fn handle_kick(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_kick(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let reason = if args.len() > 1 { + args[1..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let mut done = 0usize; + for uid in &targets { + if guild_id + .kick_with_reason(&ctx.http, *uid, &reason) + .await + .is_ok() + { + done += 1; + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "kick", + &reason, + None, + None, + ) + .await; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Kick") + .description(format!("{} membre(s) expulse(s).", done)) + .color(theme_color(ctx).await), + ) + .await; } pub struct KickCommand; pub static COMMAND_DESCRIPTOR: KickCommand = KickCommand; impl crate::commands::command_contract::CommandSpec for KickCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "kick", - command: "kick", + name: "kick", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Expulse un membre", description: "Kick un ou plusieurs membres.", examples: &["+kick @User"], - alias_source_key: "kick", default_aliases: &["k"], default_permission: 8, } diff --git a/src/commands/admin/leave.rs b/src/commands/admin/leave.rs index ccf01be..a83b8b1 100644 --- a/src/commands/admin/leave.rs +++ b/src/commands/admin/leave.rs @@ -34,14 +34,12 @@ pub static COMMAND_DESCRIPTOR: LeaveCommand = LeaveCommand; impl crate::commands::command_contract::CommandSpec for LeaveCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "leave", - command: "leave", + name: "leave", category: "admin", params: "[ID_serveur/index]", summary: "Fait quitter un serveur", description: "Force le bot a quitter un serveur cible ou le serveur courant.", examples: &["+leave", "+le", "+help leave"], - alias_source_key: "leave", default_aliases: &["lvg"], default_permission: 9, } diff --git a/src/commands/admin/leave_settings.rs b/src/commands/admin/leave_settings.rs index dfe0084..76f41fd 100644 --- a/src/commands/admin/leave_settings.rs +++ b/src/commands/admin/leave_settings.rs @@ -1,10 +1,123 @@ use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, pool}; pub async fn handle_leave_settings(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_join_leave_settings(ctx, msg, args, "leave").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + if args.is_empty() || !args[0].eq_ignore_ascii_case("settings") { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("leave settings") + .description("Usage: +leave settings [on/off] [salon] [message...]") + .color(0xED4245), + ) + .await; + return; + } + + if args.len() == 1 { + let row = sqlx::query_as::<_, (bool, Option, Option)>( + r#" + SELECT enabled, channel_id, custom_message + FROM bot_join_leave_settings + WHERE bot_id = $1 AND guild_id = $2 AND kind = $3 + LIMIT 1; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind("leave") + .fetch_optional(&pool) + .await + .ok() + .flatten(); + + let desc = if let Some((enabled, channel_id, custom_message)) = row { + format!( + "Etat: {}\nSalon: {}\nMessage: {}", + if enabled { "on" } else { "off" }, + channel_id + .map(|id| format!("<#{}>", id)) + .unwrap_or_else(|| "non defini".to_string()), + custom_message.unwrap_or_else(|| "(defaut)".to_string()) + ) + } else { + "Aucun reglage configure.".to_string() + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("leave settings") + .description(desc) + .color(theme_color(ctx).await), + ) + .await; + return; + } + + let action = args[1].to_lowercase(); + let enabled = action == "on"; + let channel = if enabled { + parse_target_channel(msg, args, 2) + } else { + None + }; + let message_start = if enabled { 3 } else { 2 }; + let custom_message = if args.len() > message_start { + Some(args[message_start..].join(" ")) + } else { + None + }; + + let _ = sqlx::query( + r#" + INSERT INTO bot_join_leave_settings (bot_id, guild_id, kind, enabled, channel_id, custom_message) + VALUES ($1, $2, $3, $4, $5, $6) + ON CONFLICT (bot_id, guild_id, kind) + DO UPDATE SET enabled = EXCLUDED.enabled, channel_id = EXCLUDED.channel_id, + custom_message = EXCLUDED.custom_message, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind("leave") + .bind(enabled) + .bind(channel.map(|c| c.get() as i64)) + .bind(custom_message) + .execute(&pool) + .await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("leave settings") + .description(format!( + "{} {}", + if enabled { "Active" } else { "Desactive" }, + channel + .map(|c| format!("dans <#{}>", c.get())) + .unwrap_or_default() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct LeaveSettingsCommand; @@ -13,8 +126,7 @@ pub static COMMAND_DESCRIPTOR: LeaveSettingsCommand = LeaveSettingsCommand; impl crate::commands::command_contract::CommandSpec for LeaveSettingsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "leave_settings", - command: "leave settings", + name: "leave_settings", category: "admin", params: "settings [on/off] [salon] [message]", summary: "Parametre les actions de leave", @@ -23,7 +135,6 @@ impl crate::commands::command_contract::CommandSpec for LeaveSettingsCommand { "+leave settings", "+leave settings on #logs {user} a quitte", ], - alias_source_key: "leave_settings", default_aliases: &["lset"], default_permission: 8, } diff --git a/src/commands/admin/lock.rs b/src/commands/admin/lock.rs index eba31fd..ab7d169 100644 --- a/src/commands/admin/lock.rs +++ b/src/commands/admin/lock.rs @@ -1,22 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_lock(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_lock_unlock(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let target = args + .first() + .and_then(|raw| parse_channel_id(raw)) + .unwrap_or(msg.channel_id); + + let ok = edit_channel_visibility(ctx, guild_id, target, Some(true), None).await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Lock") + .description(if ok { + format!("Salon <#{}> mis a jour.", target.get()) + } else { + "Echec de mise a jour du salon.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct LockCommand; pub static COMMAND_DESCRIPTOR: LockCommand = LockCommand; impl crate::commands::command_contract::CommandSpec for LockCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "lock", - command: "lock", + name: "lock", category: "admin", params: "[salon]", summary: "Ferme un salon", description: "Verrouille un salon texte ou vocal.", examples: &["+lock", "+lock #general"], - alias_source_key: "lock", default_aliases: &["lk"], default_permission: 8, } diff --git a/src/commands/admin/lockall.rs b/src/commands/admin/lockall.rs index 98164e4..8752a5f 100644 --- a/src/commands/admin/lockall.rs +++ b/src/commands/admin/lockall.rs @@ -1,22 +1,46 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_lockall(ctx: &Context, msg: &Message) { - moderation_tools::handle_lockall_unlockall(ctx, msg, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return; + }; + + let mut changed = 0usize; + for channel_id in channels.keys() { + if edit_channel_visibility(ctx, guild_id, *channel_id, Some(true), None).await { + changed += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("LockAll") + .description(format!("{} salon(s) mis a jour.", changed)) + .color(theme_color(ctx).await), + ) + .await; } pub struct LockallCommand; pub static COMMAND_DESCRIPTOR: LockallCommand = LockallCommand; impl crate::commands::command_contract::CommandSpec for LockallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "lockall", - command: "lockall", + name: "lockall", category: "admin", params: "aucun", summary: "Ferme tous les salons", description: "Verrouille tous les salons du serveur.", examples: &["+lockall"], - alias_source_key: "lockall", default_aliases: &["lka"], default_permission: 8, } diff --git a/src/commands/admin/massiverole.rs b/src/commands/admin/massiverole.rs index ba248a9..6c64c75 100644 --- a/src/commands/admin/massiverole.rs +++ b/src/commands/admin/massiverole.rs @@ -1,10 +1,58 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_role, send_embed, theme_color}; pub async fn handle_massiverole(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_massive_role(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() { + return; + } + + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + + let Some(target_role) = parse_role(&guild, args[0]) else { + return; + }; + + let filter_role = args.get(1).and_then(|raw| parse_role(&guild, raw)); + + let Ok(members) = guild_id.members(&ctx.http, None, None).await else { + return; + }; + + let mut affected = 0usize; + for member in members { + if let Some(filter) = &filter_role { + if !member.roles.contains(&filter.id) { + continue; + } + } + + if member.add_role(&ctx.http, target_role.id).await.is_ok() { + affected += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("MassiveRole") + .description(format!( + "{} membres traités pour le rôle <@&{}>.", + affected, + target_role.id.get() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct MassiveRoleCommand; @@ -13,14 +61,12 @@ pub static COMMAND_DESCRIPTOR: MassiveRoleCommand = MassiveRoleCommand; impl crate::commands::command_contract::CommandSpec for MassiveRoleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "massiverole", - command: "massiverole", + name: "massiverole", category: "admin", params: " [role_filtre]", summary: "Ajoute un role en masse", description: "Ajoute un role a tous les membres ou a ceux qui ont deja un role filtre.", examples: &["+massiverole @VIP", "+massiverole @VIP @Membres"], - alias_source_key: "massiverole", default_aliases: &["mrole", "mr"], default_permission: 8, } diff --git a/src/commands/admin/messagelog.rs b/src/commands/admin/messagelog.rs index 0b3b905..e5dec47 100644 --- a/src/commands/admin/messagelog.rs +++ b/src/commands/admin/messagelog.rs @@ -1,9 +1,55 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_messagelog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_log_toggle(ctx, msg, args, "message", "MessageLog").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("MessageLog") + .description("Usage: +messagelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + match action.as_str() { + "on" => { + let channel = parse_target_channel(msg, args, 1); + set_log_channel(ctx, guild_id, "message", channel, true).await; + let embed = CreateEmbed::new() + .title("MessageLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + "off" => { + set_log_channel(ctx, guild_id, "message", None, false).await; + let embed = CreateEmbed::new() + .title("MessageLog") + .description("Desactive.") + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + _ => { + let embed = CreateEmbed::new() + .title("MessageLog") + .description("Usage: +messagelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + } + } } pub struct MessagelogCommand; @@ -12,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: MessagelogCommand = MessagelogCommand; impl crate::commands::command_contract::CommandSpec for MessagelogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "messagelog", - command: "messagelog", + name: "messagelog", category: "admin", params: "", summary: "Active les logs de messages", description: "Active ou desactive les logs des messages supprimes et edites.", examples: &["+messagelog on #logs", "+messagelog off"], - alias_source_key: "messagelog", default_aliases: &["msglog"], default_permission: 8, } diff --git a/src/commands/admin/modlog.rs b/src/commands/admin/modlog.rs index eaf2da7..42d71b4 100644 --- a/src/commands/admin/modlog.rs +++ b/src/commands/admin/modlog.rs @@ -1,9 +1,55 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_modlog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_log_toggle(ctx, msg, args, "moderation", "ModLog").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("ModLog") + .description("Usage: +modlog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + match action.as_str() { + "on" => { + let channel = parse_target_channel(msg, args, 1); + set_log_channel(ctx, guild_id, "moderation", channel, true).await; + let embed = CreateEmbed::new() + .title("ModLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + "off" => { + set_log_channel(ctx, guild_id, "moderation", None, false).await; + let embed = CreateEmbed::new() + .title("ModLog") + .description("Desactive.") + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + _ => { + let embed = CreateEmbed::new() + .title("ModLog") + .description("Usage: +modlog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + } + } } pub struct ModlogCommand; @@ -12,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: ModlogCommand = ModlogCommand; impl crate::commands::command_contract::CommandSpec for ModlogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "modlog", - command: "modlog", + name: "modlog", category: "admin", params: "", summary: "Active les logs de moderation", description: "Active ou desactive les logs de moderation dans un salon cible.", examples: &["+modlog on #logs", "+modlog off"], - alias_source_key: "modlog", default_aliases: &["mlog"], default_permission: 8, } diff --git a/src/commands/admin/mute.rs b/src/commands/admin/mute.rs index 0bf69da..b7e8339 100644 --- a/src/commands/admin/mute.rs +++ b/src/commands/admin/mute.rs @@ -1,22 +1,68 @@ -use crate::commands::moderation_tools; +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{add_sanction, handle_timeout, parse_targets}; + pub async fn handle_mute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_mute(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let expires_at = Some(Utc::now() + chrono::Duration::seconds(28 * 24 * 3600)); + let reason = if args.len() > 1 { + args[1..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let affected = handle_timeout(ctx, guild_id, &targets, expires_at).await; + + for uid in &targets { + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "mute", + &reason, + None, + expires_at, + ) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Mute") + .description(format!("{} membre(s) mute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct MuteCommand; pub static COMMAND_DESCRIPTOR: MuteCommand = MuteCommand; impl crate::commands::command_contract::CommandSpec for MuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "mute", - command: "mute", + name: "mute", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Mute un membre", description: "Applique un mute a un ou plusieurs membres.", examples: &["+mute @User abus"], - alias_source_key: "mute", default_aliases: &["tmute"], default_permission: 8, } diff --git a/src/commands/admin/mutelist.rs b/src/commands/admin/mutelist.rs index 63fbe1e..f6350d5 100644 --- a/src/commands/admin/mutelist.rs +++ b/src/commands/admin/mutelist.rs @@ -1,22 +1,73 @@ -use crate::commands::moderation_tools; +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::pool; + pub async fn handle_mutelist(ctx: &Context, msg: &Message) { - moderation_tools::handle_mutelist(ctx, msg).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let rows = sqlx::query_as::<_, (i64, String, Option, Option>)>( + r#" + SELECT user_id, kind, channel_id, expires_at + FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute') + ORDER BY created_at DESC + LIMIT 60; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_all(&pool) + .await + .unwrap_or_default(); + + let desc = if rows.is_empty() { + "Aucun mute en cours.".to_string() + } else { + rows.into_iter() + .map(|(uid, kind, channel_id, exp)| { + let channel = channel_id + .map(|c| format!(" dans <#{}>", c)) + .unwrap_or_default(); + let until = exp + .map(|d| format!(" jusqu'a ", d.timestamp())) + .unwrap_or_default(); + format!("- <@{}> `{}`{}{}", uid, kind, channel, until) + }) + .collect::>() + .join("\n") + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("MuteList") + .description(desc) + .color(theme_color(ctx).await), + ) + .await; } pub struct MutelistCommand; pub static COMMAND_DESCRIPTOR: MutelistCommand = MutelistCommand; impl crate::commands::command_contract::CommandSpec for MutelistCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "mutelist", - command: "mutelist", + name: "mutelist", category: "admin", params: "aucun", summary: "Liste les mutes", description: "Affiche tous les mutes en cours.", examples: &["+mutelist"], - alias_source_key: "mutelist", default_aliases: &["ml"], default_permission: 8, } diff --git a/src/commands/admin/newsticker.rs b/src/commands/admin/newsticker.rs index ed44891..e2fbf87 100644 --- a/src/commands/admin/newsticker.rs +++ b/src/commands/admin/newsticker.rs @@ -1,10 +1,20 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_newsticker(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_new_sticker(ctx, msg, args).await; + let _ = args; + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("NewSticker") + .description("Creation de sticker disponible prochainement (API sticker V2).") + .color(theme_color(ctx).await), + ) + .await; } pub struct NewStickerCommand; @@ -13,14 +23,12 @@ pub static COMMAND_DESCRIPTOR: NewStickerCommand = NewStickerCommand; impl crate::commands::command_contract::CommandSpec for NewStickerCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "newsticker", - command: "newsticker", + name: "newsticker", category: "admin", params: "[nom]", summary: "Cree un sticker serveur", description: "Cree un nouveau sticker a partir d'un sticker ou fichier repondu.", examples: &["+newsticker cool_pack"], - alias_source_key: "newsticker", default_aliases: &["stcreate", "nst"], default_permission: 8, } diff --git a/src/commands/admin/nolog.rs b/src/commands/admin/nolog.rs index 9e01541..6e489e5 100644 --- a/src/commands/admin/nolog.rs +++ b/src/commands/admin/nolog.rs @@ -1,9 +1,106 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, pool}; + pub async fn handle_nolog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_nolog(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("NoLog") + .description("Usage: +nolog [salon] [message|voice|all]") + .color(0xED4245), + ) + .await; + return; + } + + let action = args[0].to_lowercase(); + let channel = parse_target_channel(msg, args, 1).unwrap_or(msg.channel_id); + let scope = args + .get(2) + .map(|s| s.to_lowercase()) + .unwrap_or_else(|| "all".to_string()); + + let set_message = scope == "all" || scope == "message"; + let set_voice = scope == "all" || scope == "voice"; + + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + if action == "add" { + let _ = sqlx::query( + r#" + INSERT INTO bot_nolog_channels (bot_id, guild_id, channel_id, disable_message, disable_voice) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (bot_id, guild_id, channel_id) + DO UPDATE SET disable_message = bot_nolog_channels.disable_message OR EXCLUDED.disable_message, + disable_voice = bot_nolog_channels.disable_voice OR EXCLUDED.disable_voice, + updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel.get() as i64) + .bind(set_message) + .bind(set_voice) + .execute(&pool) + .await; + } else if action == "del" { + let _ = sqlx::query( + r#" + UPDATE bot_nolog_channels + SET disable_message = CASE WHEN $4 THEN FALSE ELSE disable_message END, + disable_voice = CASE WHEN $5 THEN FALSE ELSE disable_voice END, + updated_at = NOW() + WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel.get() as i64) + .bind(set_message) + .bind(set_voice) + .execute(&pool) + .await; + + let _ = sqlx::query( + r#" + DELETE FROM bot_nolog_channels + WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3 + AND disable_message = FALSE AND disable_voice = FALSE; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel.get() as i64) + .execute(&pool) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("NoLog") + .description(format!( + "{} applique sur <#{}> ({})", + action, + channel.get(), + scope + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct NologCommand; @@ -12,14 +109,12 @@ pub static COMMAND_DESCRIPTOR: NologCommand = NologCommand; impl crate::commands::command_contract::CommandSpec for NologCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "nolog", - command: "nolog", + name: "nolog", category: "admin", params: " [salon] [message|voice|all]", summary: "Exclut des salons des logs", description: "Desactive ou reactive les logs message/voice pour certains salons.", examples: &["+nolog add #secret all", "+nolog del #secret message"], - alias_source_key: "nolog", default_aliases: &["nlg"], default_permission: 8, } diff --git a/src/commands/admin/owner.rs b/src/commands/admin/owner.rs index 21e69ba..e831b18 100644 --- a/src/commands/admin/owner.rs +++ b/src/commands/admin/owner.rs @@ -45,14 +45,12 @@ pub static COMMAND_DESCRIPTOR: OwnerCommand = OwnerCommand; impl crate::commands::command_contract::CommandSpec for OwnerCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "owner", - command: "owner", + name: "owner", category: "admin", params: "aucun", summary: "Liste les owners du bot", description: "Affiche l owner application et les owners ajoutes en base.", examples: &["+owner", "+or", "+help owner"], - alias_source_key: "owner", default_aliases: &["own"], default_permission: 9, } diff --git a/src/commands/admin/raidlog.rs b/src/commands/admin/raidlog.rs index 71a8889..3a03a50 100644 --- a/src/commands/admin/raidlog.rs +++ b/src/commands/admin/raidlog.rs @@ -1,9 +1,49 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_raidlog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_raidlog(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args + .first() + .map(|a| a.eq_ignore_ascii_case("off")) + .unwrap_or(false) + { + set_log_channel(ctx, guild_id, "raid", None, false).await; + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("RaidLog") + .description("Desactive.") + .color(theme_color(ctx).await), + ) + .await; + return; + } + + let channel = parse_target_channel(msg, args, 0); + set_log_channel(ctx, guild_id, "raid", channel, true).await; + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("RaidLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct RaidlogCommand; @@ -12,14 +52,12 @@ pub static COMMAND_DESCRIPTOR: RaidlogCommand = RaidlogCommand; impl crate::commands::command_contract::CommandSpec for RaidlogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "raidlog", - command: "raidlog", + name: "raidlog", category: "admin", params: "[salon]|off", summary: "Active les logs antiraid", description: "Active les logs antiraid dans un salon ou les desactive.", examples: &["+raidlog #logs", "+raidlog off"], - alias_source_key: "raidlog", default_aliases: &["rdlog"], default_permission: 8, } diff --git a/src/commands/admin/rename.rs b/src/commands/admin/rename.rs index 6d60e51..fb0206d 100644 --- a/src/commands/admin/rename.rs +++ b/src/commands/admin/rename.rs @@ -122,3 +122,21 @@ pub async fn handle_rename(ctx: &Context, msg: &Message, args: &[&str]) { ) .await; } + +pub struct RenameCommand; +pub static COMMAND_DESCRIPTOR: RenameCommand = RenameCommand; + +impl crate::commands::command_contract::CommandSpec for RenameCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "rename", + category: "admin", + params: "", + summary: "Renomme le ticket courant", + description: "Renomme le salon du ticket et met a jour son titre en base.", + examples: &["+rename support-client", "+help rename"], + default_aliases: &[], + default_permission: 2, + } + } +} diff --git a/src/commands/admin/renew.rs b/src/commands/admin/renew.rs index 631a48e..ec0cdde 100644 --- a/src/commands/admin/renew.rs +++ b/src/commands/admin/renew.rs @@ -1,10 +1,51 @@ +use serenity::builder::CreateChannel; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::parse_channel_id; pub async fn handle_renew(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_renew(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let channel_id = args + .first() + .and_then(|raw| parse_channel_id(raw)) + .unwrap_or(msg.channel_id); + + let Ok(channel) = channel_id.to_channel(&ctx.http).await else { + return; + }; + let Channel::Guild(text_channel) = channel else { + return; + }; + + if text_channel.kind != ChannelType::Text && text_channel.kind != ChannelType::News { + return; + } + + let parent_id = text_channel.parent_id; + let topic = text_channel.topic.clone(); + let nsfw = text_channel.nsfw; + let slowmode = text_channel.rate_limit_per_user; + let name = text_channel.name.clone(); + + let _ = text_channel.delete(&ctx.http).await; + + let mut builder = CreateChannel::new(name) + .kind(ChannelType::Text) + .nsfw(nsfw) + .rate_limit_per_user(slowmode.unwrap_or(0)); + + if let Some(parent) = parent_id { + builder = builder.category(parent); + } + if let Some(topic) = topic { + builder = builder.topic(topic); + } + + let _ = guild_id.create_channel(&ctx.http, builder).await; } pub struct RenewCommand; @@ -13,14 +54,12 @@ pub static COMMAND_DESCRIPTOR: RenewCommand = RenewCommand; impl crate::commands::command_contract::CommandSpec for RenewCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "renew", - command: "renew", + name: "renew", category: "admin", params: "[salon]", summary: "Recree un salon textuel", description: "Supprime puis recree un salon textuel en conservant les options principales.", examples: &["+renew", "+renew #general"], - alias_source_key: "renew", default_aliases: &["nuke", "rebuildch"], default_permission: 8, } diff --git a/src/commands/admin/reroll.rs b/src/commands/admin/reroll.rs index 32b21d9..69187a5 100644 --- a/src/commands/admin/reroll.rs +++ b/src/commands/admin/reroll.rs @@ -1,10 +1,55 @@ +use rand::seq::SliceRandom; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; -pub async fn handle_reroll(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_reroll(ctx, msg, args).await; +pub async fn handle_reroll(ctx: &Context, msg: &Message, _args: &[&str]) { + let Some(referenced) = msg.referenced_message.as_ref() else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Reroll") + .description("Réponds à un message giveaway pour reroll.") + .color(0xED4245), + ) + .await; + return; + }; + + let mut candidates = referenced.mentions.iter().map(|u| u.id).collect::>(); + candidates.sort_by_key(|u| u.get()); + candidates.dedup(); + + if candidates.is_empty() { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Reroll") + .description("Aucun participant détecté.") + .color(0xED4245), + ) + .await; + return; + } + + let winner = candidates + .choose(&mut rand::thread_rng()) + .copied() + .unwrap_or(candidates[0]); + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Reroll") + .description(format!("Nouveau gagnant: <@{}>", winner.get())) + .color(theme_color(ctx).await), + ) + .await; } pub struct RerollCommand; @@ -13,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: RerollCommand = RerollCommand; impl crate::commands::command_contract::CommandSpec for RerollCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "reroll", - command: "reroll", + name: "reroll", category: "admin", params: "aucun (en reponse a un message)", summary: "Relance un tirage giveaway", description: "Choisit un nouveau gagnant depuis le message cible.", examples: &["+reroll"], - alias_source_key: "reroll", default_aliases: &["rro", "greroll"], default_permission: 8, } diff --git a/src/commands/admin/rolelog.rs b/src/commands/admin/rolelog.rs index fe83af5..d20d4e7 100644 --- a/src/commands/admin/rolelog.rs +++ b/src/commands/admin/rolelog.rs @@ -1,9 +1,55 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_rolelog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_log_toggle(ctx, msg, args, "role", "RoleLog").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("RoleLog") + .description("Usage: +rolelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + match action.as_str() { + "on" => { + let channel = parse_target_channel(msg, args, 1); + set_log_channel(ctx, guild_id, "role", channel, true).await; + let embed = CreateEmbed::new() + .title("RoleLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + "off" => { + set_log_channel(ctx, guild_id, "role", None, false).await; + let embed = CreateEmbed::new() + .title("RoleLog") + .description("Desactive.") + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + _ => { + let embed = CreateEmbed::new() + .title("RoleLog") + .description("Usage: +rolelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + } + } } pub struct RolelogCommand; @@ -12,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: RolelogCommand = RolelogCommand; impl crate::commands::command_contract::CommandSpec for RolelogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "rolelog", - command: "rolelog", + name: "rolelog", category: "admin", params: "", summary: "Active les logs de roles", description: "Active ou desactive les logs des roles.", examples: &["+rolelog on #logs", "+rolelog off"], - alias_source_key: "rolelog", default_aliases: &["rlog"], default_permission: 8, } diff --git a/src/commands/admin/sanctions.rs b/src/commands/admin/sanctions.rs index ad7daca..41197ef 100644 --- a/src/commands/admin/sanctions.rs +++ b/src/commands/admin/sanctions.rs @@ -1,10 +1,98 @@ +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::moderation_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{send_embed, theme_color}; +use crate::db::DbPoolKey; pub async fn handle_sanctions(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_sanctions(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Some(target_raw) = args.first() else { + let _ = send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Sanctions") + .description("Usage: +sanctions ") + .color(0xED4245), + ) + .await; + return; + }; + let Some(target) = parse_user_id(target_raw) else { + return; + }; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + let Some(pool) = pool else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let rows = sqlx::query_as::< + _, + ( + i64, + String, + String, + chrono::DateTime, + Option>, + bool, + ), + >( + r#" + SELECT id, kind, reason, created_at, expires_at, active + FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 + ORDER BY created_at DESC + LIMIT 30; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(target.get() as i64) + .fetch_all(&pool) + .await + .unwrap_or_default(); + + let desc = if rows.is_empty() { + "Aucune sanction.".to_string() + } else { + rows.into_iter() + .map(|(id, kind, reason, created_at, expires_at, active)| { + let until = expires_at + .map(|d| format!(" · jusqu'à ", d.timestamp())) + .unwrap_or_default(); + format!( + "`#{}` `{}` {} · {} · {}", + id, + kind, + if active { "(active)" } else { "(inactive)" }, + created_at.timestamp(), + until, + reason + ) + }) + .collect::>() + .join("\n") + }; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title(format!("Sanctions de <@{}>", target.get())) + .description(desc) + .color(theme_color(ctx).await), + ) + .await; } pub struct SanctionsCommand; @@ -13,14 +101,12 @@ pub static COMMAND_DESCRIPTOR: SanctionsCommand = SanctionsCommand; impl crate::commands::command_contract::CommandSpec for SanctionsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "sanctions", - command: "sanctions", + name: "sanctions", category: "admin", params: "<@membre/ID>", summary: "Affiche les sanctions d un membre", description: "Liste l historique des sanctions d un membre.", examples: &["+sanctions @User"], - alias_source_key: "sanctions", default_aliases: &["sanct"], default_permission: 8, } diff --git a/src/commands/admin/say.rs b/src/commands/admin/say.rs index 8f6ed13..6c147a1 100644 --- a/src/commands/admin/say.rs +++ b/src/commands/admin/say.rs @@ -27,14 +27,12 @@ pub static COMMAND_DESCRIPTOR: SayCommand = SayCommand; impl crate::commands::command_contract::CommandSpec for SayCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "say", - command: "say", + name: "say", category: "admin", params: "", summary: "Fait parler le bot", description: "Envoie un message brut dans le salon courant via le bot.", examples: &["+say", "+sy", "+help say"], - alias_source_key: "say", default_aliases: &["sym"], default_permission: 8, } diff --git a/src/commands/admin/set_boostembed.rs b/src/commands/admin/set_boostembed.rs index 2fe9b46..8b4bfb4 100644 --- a/src/commands/admin/set_boostembed.rs +++ b/src/commands/admin/set_boostembed.rs @@ -1,10 +1,96 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::logs_service; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::pool; pub async fn handle_set_boostembed(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_set_boostembed(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 2 { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Set BoostEmbed") + .description("Usage: +set boostembed ") + .color(0xED4245), + ) + .await; + return; + } + + let field = args[0].to_lowercase(); + let value = args[1..].join(" "); + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let _ = sqlx::query( + r#" + INSERT INTO bot_boost_embed (bot_id, guild_id, enabled, title, description, color) + VALUES ($1, $2, TRUE, NULL, NULL, NULL) + ON CONFLICT (bot_id, guild_id) + DO NOTHING; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .execute(&pool) + .await; + + match field.as_str() { + "title" => { + let _ = sqlx::query( + "UPDATE bot_boost_embed SET title = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(value) + .execute(&pool) + .await; + } + "description" => { + let _ = sqlx::query( + "UPDATE bot_boost_embed SET description = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(value) + .execute(&pool) + .await; + } + "color" => { + let normalized = value + .trim() + .trim_start_matches('#') + .trim_start_matches("0x"); + if let Ok(color) = u32::from_str_radix(normalized, 16) { + let _ = sqlx::query( + "UPDATE bot_boost_embed SET color = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(color as i32) + .execute(&pool) + .await; + } + } + _ => {} + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Set BoostEmbed") + .description("Configuration mise a jour.") + .color(theme_color(ctx).await), + ) + .await; } pub struct SetBoostembedCommand; @@ -13,8 +99,7 @@ pub static COMMAND_DESCRIPTOR: SetBoostembedCommand = SetBoostembedCommand; impl crate::commands::command_contract::CommandSpec for SetBoostembedCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "set_boostembed", - command: "set boostembed", + name: "set_boostembed", category: "admin", params: " ", summary: "Parametre l embed de boost", @@ -23,7 +108,6 @@ impl crate::commands::command_contract::CommandSpec for SetBoostembedCommand { "+set boostembed title Merci", "+set boostembed color #FF66CC", ], - alias_source_key: "set_boostembed", default_aliases: &["sboostembed"], default_permission: 8, } diff --git a/src/commands/admin/set_modlogs.rs b/src/commands/admin/set_modlogs.rs index 67d8516..a34bb61 100644 --- a/src/commands/admin/set_modlogs.rs +++ b/src/commands/admin/set_modlogs.rs @@ -1,10 +1,109 @@ +use std::collections::BTreeSet; + +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::logs_service; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::pool; pub async fn handle_set_modlogs(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_set_modlogs(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let row = sqlx::query_as::<_, (String,)>( + r#" + SELECT modlog_events + FROM bot_log_settings + WHERE bot_id = $1 AND guild_id = $2 + LIMIT 1; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_optional(&pool) + .await + .ok() + .flatten(); + + let mut events = row + .map(|(s,)| { + s.split(',') + .map(|v| v.trim().to_lowercase()) + .filter(|v| !v.is_empty()) + .collect::>() + }) + .unwrap_or_else(|| { + [ + "warn", + "mute", + "tempmute", + "unmute", + "cmute", + "tempcmute", + "uncmute", + "kick", + "ban", + "tempban", + "unban", + "lock", + "unlock", + "hide", + "unhide", + "addrole", + "delrole", + "derank", + "clear", + "sanctions", + ] + .into_iter() + .map(|s| s.to_string()) + .collect() + }); + + if args.len() >= 2 { + let event = args[0].to_lowercase(); + let state = args[1].to_lowercase(); + if state == "on" { + events.insert(event); + } else if state == "off" { + events.remove(&event); + } + + let serialized = events.iter().cloned().collect::>().join(","); + let _ = sqlx::query( + r#" + INSERT INTO bot_log_settings (bot_id, guild_id, modlog_events) + VALUES ($1, $2, $3) + ON CONFLICT (bot_id, guild_id) + DO UPDATE SET modlog_events = EXCLUDED.modlog_events, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(serialized) + .execute(&pool) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Set ModLogs") + .description(format!( + "Evenements actifs:\n{}\n\nUsage: +set modlogs ", + events.iter().cloned().collect::>().join(", ") + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct SetModlogsCommand; @@ -13,14 +112,12 @@ pub static COMMAND_DESCRIPTOR: SetModlogsCommand = SetModlogsCommand; impl crate::commands::command_contract::CommandSpec for SetModlogsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "set_modlogs", - command: "set modlogs", + name: "set_modlogs", category: "admin", params: "[event on/off]", summary: "Parametre les evenements de modlogs", description: "Affiche ou modifie les evenements qui apparaissent dans les logs de moderation.", examples: &["+set modlogs", "+set modlogs warn off"], - alias_source_key: "set_modlogs", default_aliases: &["smodlog"], default_permission: 8, } diff --git a/src/commands/admin/suggestion.rs b/src/commands/admin/suggestion.rs index b20f6d3..53583ac 100644 --- a/src/commands/admin/suggestion.rs +++ b/src/commands/admin/suggestion.rs @@ -496,3 +496,25 @@ pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) - false } + +pub struct SuggestionCommand; +pub static COMMAND_DESCRIPTOR: SuggestionCommand = SuggestionCommand; + +impl crate::commands::command_contract::CommandSpec for SuggestionCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "suggestion", + category: "admin", + params: " | settings", + summary: "Publie ou configure les suggestions", + description: "Publie une suggestion utilisateur ou ouvre le panneau de configuration.", + examples: &[ + "+suggestion Ameliorer le salon", + "+suggestion settings", + "+help suggestion", + ], + default_aliases: &[], + default_permission: 0, + } + } +} diff --git a/src/commands/admin/sync.rs b/src/commands/admin/sync.rs index 36b9fdf..a0d5f0e 100644 --- a/src/commands/admin/sync.rs +++ b/src/commands/admin/sync.rs @@ -1,10 +1,75 @@ +use serenity::builder::{CreateEmbed, EditChannel}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; pub async fn handle_sync(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_sync(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(scope) = args.first() else { + return; + }; + + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return; + }; + + let ids_to_sync = if scope.eq_ignore_ascii_case("all") { + channels.keys().copied().collect::>() + } else if let Some(channel_id) = parse_channel_id(scope) { + if let Some(target) = channels.get(&channel_id) { + if target.kind == ChannelType::Category { + channels + .values() + .filter(|ch| ch.parent_id == Some(channel_id)) + .map(|ch| ch.id) + .collect::>() + } else { + vec![channel_id] + } + } else { + vec![channel_id] + } + } else { + Vec::new() + }; + + let mut synced = 0usize; + for channel_id in ids_to_sync { + let Some(channel) = channels.get(&channel_id) else { + continue; + }; + let Some(parent_id) = channel.parent_id else { + continue; + }; + let Some(parent) = channels.get(&parent_id) else { + continue; + }; + + if channel_id + .edit( + &ctx.http, + EditChannel::new().permissions(parent.permission_overwrites.clone()), + ) + .await + .is_ok() + { + synced += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Sync") + .description(format!("{} salons synchronisés.", synced)) + .color(theme_color(ctx).await), + ) + .await; } pub struct SyncCommand; @@ -13,14 +78,12 @@ pub static COMMAND_DESCRIPTOR: SyncCommand = SyncCommand; impl crate::commands::command_contract::CommandSpec for SyncCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "sync", - command: "sync", + name: "sync", category: "admin", params: "", summary: "Synchronise les permissions", description: "Synchronise les permissions d'un salon avec sa categorie, ou tous les salons avec all.", examples: &["+sync all", "+sync #general"], - alias_source_key: "sync", default_aliases: &["chsync"], default_permission: 8, } diff --git a/src/commands/admin/tempban.rs b/src/commands/admin/tempban.rs index 3a69eff..84b3f5c 100644 --- a/src/commands/admin/tempban.rs +++ b/src/commands/admin/tempban.rs @@ -1,22 +1,83 @@ -use crate::commands::moderation_tools; +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{ + add_sanction, duration_from_input, parse_targets, +}; + pub async fn handle_tempban(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_ban(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let Some(duration_raw) = args.get(1) else { + return; + }; + let Some(duration) = duration_from_input(duration_raw) else { + return; + }; + let expires_at = Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)); + + let reason = if args.len() > 2 { + args[2..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let mut done = 0usize; + for uid in &targets { + if guild_id + .ban_with_reason(&ctx.http, *uid, 0, &reason) + .await + .is_ok() + { + done += 1; + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "tempban", + &reason, + None, + expires_at, + ) + .await; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("TempBan") + .description(format!("{} membre(s) banni(s).", done)) + .color(theme_color(ctx).await), + ) + .await; } pub struct TempbanCommand; pub static COMMAND_DESCRIPTOR: TempbanCommand = TempbanCommand; impl crate::commands::command_contract::CommandSpec for TempbanCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "tempban", - command: "tempban", + name: "tempban", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Ban temporaire", description: "Ban temporairement un ou plusieurs membres.", examples: &["+tempban @User 1d"], - alias_source_key: "tempban", default_aliases: &["tb"], default_permission: 8, } diff --git a/src/commands/admin/tempcmute.rs b/src/commands/admin/tempcmute.rs index 29142cd..ffdc2d7 100644 --- a/src/commands/admin/tempcmute.rs +++ b/src/commands/admin/tempcmute.rs @@ -1,22 +1,77 @@ -use crate::commands::moderation_tools; +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{ + add_sanction, channel_mute_users, duration_from_input, parse_targets, +}; + pub async fn handle_tempcmute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_cmute(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let Some(duration_raw) = args.get(1) else { + return; + }; + let Some(duration) = duration_from_input(duration_raw) else { + return; + }; + let expires_at = Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)); + + let reason = if args.len() > 2 { + args[2..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let affected = channel_mute_users(ctx, msg.channel_id, &targets, true).await; + + for uid in &targets { + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "tempcmute", + &reason, + Some(msg.channel_id), + expires_at, + ) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("TempCMute") + .description(format!("{} membre(s) cmute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct TempcmuteCommand; pub static COMMAND_DESCRIPTOR: TempcmuteCommand = TempcmuteCommand; impl crate::commands::command_contract::CommandSpec for TempcmuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "tempcmute", - command: "tempcmute", + name: "tempcmute", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Mute salon temporaire", description: "Mute temporaire sur le salon courant.", examples: &["+tempcmute @User 5m"], - alias_source_key: "tempcmute", default_aliases: &["tcm"], default_permission: 8, } diff --git a/src/commands/admin/tempmute.rs b/src/commands/admin/tempmute.rs index 3213aee..028c399 100644 --- a/src/commands/admin/tempmute.rs +++ b/src/commands/admin/tempmute.rs @@ -1,22 +1,77 @@ -use crate::commands::moderation_tools; +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{ + add_sanction, duration_from_input, handle_timeout, parse_targets, +}; + pub async fn handle_tempmute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_mute(ctx, msg, args, true).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let Some(duration_raw) = args.get(1) else { + return; + }; + let Some(duration) = duration_from_input(duration_raw) else { + return; + }; + let expires_at = Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)); + + let reason = if args.len() > 2 { + args[2..].join(" ") + } else { + "Aucune raison".to_string() + }; + + let affected = handle_timeout(ctx, guild_id, &targets, expires_at).await; + + for uid in &targets { + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "tempmute", + &reason, + None, + expires_at, + ) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("TempMute") + .description(format!("{} membre(s) mute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct TempmuteCommand; pub static COMMAND_DESCRIPTOR: TempmuteCommand = TempmuteCommand; impl crate::commands::command_contract::CommandSpec for TempmuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "tempmute", - command: "tempmute", + name: "tempmute", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Mute temporaire", description: "Mute un ou plusieurs membres pour une duree donnee.", examples: &["+tempmute @User 10m"], - alias_source_key: "tempmute", default_aliases: &["tm"], default_permission: 8, } diff --git a/src/commands/admin/temprole.rs b/src/commands/admin/temprole.rs index d2162a2..6f86f87 100644 --- a/src/commands/admin/temprole.rs +++ b/src/commands/admin/temprole.rs @@ -1,10 +1,113 @@ +use chrono::Utc; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use std::time::Duration; -use crate::commands::advanced_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{parse_role, send_embed, theme_color}; +use crate::db::DbPoolKey; + +fn duration_from_input(input: &str) -> Option { + let raw = input.trim().to_lowercase(); + if raw.is_empty() { + return None; + } + + let mut number = String::new(); + let mut suffix = String::new(); + for ch in raw.chars() { + if ch.is_ascii_digit() { + if !suffix.is_empty() { + return None; + } + number.push(ch); + } else if !ch.is_whitespace() { + suffix.push(ch); + } + } + + let value = number.parse::().ok()?; + let secs = match suffix.as_str() { + "s" | "sec" | "secs" | "seconde" | "secondes" => value, + "m" | "min" | "mins" | "minute" | "minutes" => value * 60, + "h" | "heure" | "heures" => value * 3600, + "j" | "d" | "jour" | "jours" => value * 86400, + _ => return None, + }; + + Some(Duration::from_secs(secs.max(1))) +} pub async fn handle_temprole(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_temprole(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.len() < 3 { + return; + } + + let Some(user_id) = parse_user_id(args[0]) else { + return; + }; + + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + + let Some(role) = parse_role(&guild, args[1]) else { + return; + }; + + let Some(duration) = duration_from_input(args[2]) else { + return; + }; + + let expires_at = Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64); + + if let Ok(member) = guild_id.member(&ctx.http, user_id).await { + let _ = member.add_role(&ctx.http, role.id).await; + } + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let bot_id = ctx.cache.current_user().id; + let _ = sqlx::query( + r#" + INSERT INTO bot_temproles (bot_id, guild_id, user_id, role_id, expires_at, active, added_by) + VALUES ($1, $2, $3, $4, $5, TRUE, $6) + ON CONFLICT (bot_id, guild_id, user_id, role_id) + DO UPDATE SET expires_at = EXCLUDED.expires_at, active = TRUE, added_by = EXCLUDED.added_by; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(user_id.get() as i64) + .bind(role.id.get() as i64) + .bind(expires_at) + .bind(msg.author.id.get() as i64) + .execute(&pool) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("TempRole") + .description(format!( + "Rôle <@&{}> ajouté à <@{}> jusqu'à .", + role.id.get(), + user_id.get(), + expires_at.timestamp() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct TempRoleCommand; @@ -13,14 +116,12 @@ pub static COMMAND_DESCRIPTOR: TempRoleCommand = TempRoleCommand; impl crate::commands::command_contract::CommandSpec for TempRoleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "temprole", - command: "temprole", + name: "temprole", category: "admin", params: " ", summary: "Ajoute un role temporaire", description: "Attribue un role a un membre pour une duree donnee puis le retire automatiquement.", examples: &["+temprole @User @VIP 2h"], - alias_source_key: "temprole", default_aliases: &["trole", "tmprole"], default_permission: 8, } diff --git a/src/commands/admin/tempvoc.rs b/src/commands/admin/tempvoc.rs index 95f168e..81fb879 100644 --- a/src/commands/admin/tempvoc.rs +++ b/src/commands/admin/tempvoc.rs @@ -440,3 +440,21 @@ pub async fn handle_voice_state_update(ctx: &Context, old: Option<&VoiceState>, } } } + +pub struct TempvocCommand; +pub static COMMAND_DESCRIPTOR: TempvocCommand = TempvocCommand; + +impl crate::commands::command_contract::CommandSpec for TempvocCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "tempvoc", + category: "admin", + params: "[cmd]", + summary: "Configure les vocaux temporaires", + description: "Affiche le menu de configuration du systeme de vocaux temporaires.", + examples: &["+tempvoc", "+tempvoc cmd", "+help tempvoc"], + default_aliases: &[], + default_permission: 8, + } + } +} diff --git a/src/commands/admin/tempvoc_cmd.rs b/src/commands/admin/tempvoc_cmd.rs index cd266cd..56096a2 100644 --- a/src/commands/admin/tempvoc_cmd.rs +++ b/src/commands/admin/tempvoc_cmd.rs @@ -15,3 +15,21 @@ pub async fn handle_tempvoc_cmd(ctx: &Context, msg: &Message, _args: &[&str]) { send_embed(ctx, msg, embed).await; } + +pub struct TempvocCmdCommand; +pub static COMMAND_DESCRIPTOR: TempvocCmdCommand = TempvocCmdCommand; + +impl crate::commands::command_contract::CommandSpec for TempvocCmdCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "tempvoc_cmd", + category: "admin", + params: "aucun", + summary: "Affiche laide tempvoc", + description: "Affiche un rappel des commandes et du fonctionnement de tempvoc.", + examples: &["+tempvoc cmd", "+help tempvoc_cmd"], + default_aliases: &[], + default_permission: 0, + } + } +} diff --git a/src/commands/admin/ticket.rs b/src/commands/admin/ticket.rs index b80a620..4dc6f59 100644 --- a/src/commands/admin/ticket.rs +++ b/src/commands/admin/ticket.rs @@ -441,3 +441,21 @@ pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) - false } + +pub struct TicketCommand; +pub static COMMAND_DESCRIPTOR: TicketCommand = TicketCommand; + +impl crate::commands::command_contract::CommandSpec for TicketCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "ticket", + category: "admin", + params: "settings", + summary: "Ouvre la gestion des tickets", + description: "Affiche le menu de configuration du systeme de tickets.", + examples: &["+ticket", "+help ticket"], + default_aliases: &[], + default_permission: 8, + } + } +} diff --git a/src/commands/admin/ticket_member.rs b/src/commands/admin/ticket_member.rs index 19f7689..4e6b89a 100644 --- a/src/commands/admin/ticket_member.rs +++ b/src/commands/admin/ticket_member.rs @@ -150,3 +150,21 @@ pub async fn handle_ticket_add(ctx: &Context, msg: &Message, args: &[&str]) { pub async fn handle_ticket_remove(ctx: &Context, msg: &Message, args: &[&str]) { let _ = ticket_member_update(ctx, msg, args, false).await; } + +pub struct TicketMemberCommand; +pub static COMMAND_DESCRIPTOR: TicketMemberCommand = TicketMemberCommand; + +impl crate::commands::command_contract::CommandSpec for TicketMemberCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "add", + category: "admin", + params: "<@membre/ID>", + summary: "Ajoute un membre au ticket", + description: "Ajoute un membre supplementaire au ticket courant via +add.", + examples: &["+add @User", "+help add"], + default_aliases: &[], + default_permission: 2, + } + } +} diff --git a/src/commands/admin/tickets.rs b/src/commands/admin/tickets.rs index e978180..ba701cc 100644 --- a/src/commands/admin/tickets.rs +++ b/src/commands/admin/tickets.rs @@ -66,3 +66,21 @@ pub async fn handle_tickets(ctx: &Context, msg: &Message, args: &[&str]) { ) .await; } + +pub struct TicketsCommand; +pub static COMMAND_DESCRIPTOR: TicketsCommand = TicketsCommand; + +impl crate::commands::command_contract::CommandSpec for TicketsCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "tickets", + category: "admin", + params: "[page]", + summary: "Liste les tickets", + description: "Affiche les tickets du serveur avec pagination.", + examples: &["+tickets", "+tickets 2", "+help tickets"], + default_aliases: &[], + default_permission: 2, + } + } +} diff --git a/src/commands/admin/unban.rs b/src/commands/admin/unban.rs index 64349a0..88f8dff 100644 --- a/src/commands/admin/unban.rs +++ b/src/commands/admin/unban.rs @@ -1,22 +1,51 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::parse_targets; + pub async fn handle_unban(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_unban(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let mut done = 0usize; + for uid in &targets { + if guild_id.unban(&ctx.http, *uid).await.is_ok() { + done += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnBan") + .description(format!("{} membre(s) unban.", done)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnbanCommand; pub static COMMAND_DESCRIPTOR: UnbanCommand = UnbanCommand; impl crate::commands::command_contract::CommandSpec for UnbanCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unban", - command: "unban", + name: "unban", category: "admin", params: "<@membre/ID[,..]>", summary: "Retire un ban", description: "Unban un ou plusieurs membres.", examples: &["+unban @User"], - alias_source_key: "unban", default_aliases: &["ub"], default_permission: 8, } diff --git a/src/commands/admin/unbanall.rs b/src/commands/admin/unbanall.rs index ec098fc..5ecf2d9 100644 --- a/src/commands/admin/unbanall.rs +++ b/src/commands/admin/unbanall.rs @@ -1,10 +1,35 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; -pub async fn handle_unbanall(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_unbanall(ctx, msg, args).await; +pub async fn handle_unbanall(ctx: &Context, msg: &Message, _args: &[&str]) { + let Some(guild_id) = msg.guild_id else { + return; + }; + + let bans = guild_id + .bans(&ctx.http, None, None) + .await + .unwrap_or_default(); + + let mut unbanned = 0usize; + for ban in bans { + if guild_id.unban(&ctx.http, ban.user.id).await.is_ok() { + unbanned += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnbanAll") + .description(format!("{} bannissements retirés.", unbanned)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnbanAllCommand; @@ -13,14 +38,12 @@ pub static COMMAND_DESCRIPTOR: UnbanAllCommand = UnbanAllCommand; impl crate::commands::command_contract::CommandSpec for UnbanAllCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unbanall", - command: "unbanall", + name: "unbanall", category: "admin", params: "aucun", summary: "Retire tous les bannissements", description: "Supprime tous les bans du serveur cible.", examples: &["+unbanall"], - alias_source_key: "unbanall", default_aliases: &["uball", "clearbans"], default_permission: 8, } diff --git a/src/commands/admin/unbl.rs b/src/commands/admin/unbl.rs index 98f1ad8..d127ce1 100644 --- a/src/commands/admin/unbl.rs +++ b/src/commands/admin/unbl.rs @@ -65,14 +65,12 @@ pub static COMMAND_DESCRIPTOR: UnblCommand = UnblCommand; impl crate::commands::command_contract::CommandSpec for UnblCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unbl", - command: "unbl", + name: "unbl", category: "admin", params: "<@membre/ID>", summary: "Retire un utilisateur blacklist", description: "Retire un utilisateur de la blacklist globale du bot.", examples: &["+unbl", "+ul", "+help unbl"], - alias_source_key: "unbl", default_aliases: &["unb"], default_permission: 9, } diff --git a/src/commands/admin/uncmute.rs b/src/commands/admin/uncmute.rs index 035b390..b0c149c 100644 --- a/src/commands/admin/uncmute.rs +++ b/src/commands/admin/uncmute.rs @@ -1,22 +1,65 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{channel_mute_users, parse_targets, pool}; + pub async fn handle_uncmute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_uncmute(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let affected = channel_mute_users(ctx, msg.channel_id, &targets, false).await; + + if let Some(pool) = pool(ctx).await { + let bot_id = ctx.cache.current_user().id; + for uid in &targets { + let _ = sqlx::query( + r#" + UPDATE bot_sanctions + SET active = FALSE + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND active = TRUE AND kind IN ('cmute','tempcmute') AND channel_id = $4; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(uid.get() as i64) + .bind(msg.channel_id.get() as i64) + .execute(&pool) + .await; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnCMute") + .description(format!("{} membre(s) uncmute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UncmuteCommand; pub static COMMAND_DESCRIPTOR: UncmuteCommand = UncmuteCommand; impl crate::commands::command_contract::CommandSpec for UncmuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "uncmute", - command: "uncmute", + name: "uncmute", category: "admin", params: "<@membre/ID[,..]>", summary: "Retire un cmute", description: "Met fin au mute salon.", examples: &["+uncmute @User"], - alias_source_key: "uncmute", default_aliases: &["ucm"], default_permission: 8, } diff --git a/src/commands/admin/unhide.rs b/src/commands/admin/unhide.rs index 95aded9..dbee1ed 100644 --- a/src/commands/admin/unhide.rs +++ b/src/commands/admin/unhide.rs @@ -1,22 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_unhide(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_hide_unhide(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let target = args + .first() + .and_then(|raw| parse_channel_id(raw)) + .unwrap_or(msg.channel_id); + + let ok = edit_channel_visibility(ctx, guild_id, target, None, Some(false)).await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnHide") + .description(if ok { + format!("Salon <#{}> mis a jour.", target.get()) + } else { + "Echec de mise a jour du salon.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnhideCommand; pub static COMMAND_DESCRIPTOR: UnhideCommand = UnhideCommand; impl crate::commands::command_contract::CommandSpec for UnhideCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unhide", - command: "unhide", + name: "unhide", category: "admin", params: "[salon]", summary: "Affiche un salon", description: "Rend a nouveau visible un salon.", examples: &["+unhide", "+unhide #general"], - alias_source_key: "unhide", default_aliases: &["uhd"], default_permission: 8, } diff --git a/src/commands/admin/unhideall.rs b/src/commands/admin/unhideall.rs index 90a3512..942ce88 100644 --- a/src/commands/admin/unhideall.rs +++ b/src/commands/admin/unhideall.rs @@ -1,9 +1,34 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_unhideall(ctx: &Context, msg: &Message) { - moderation_tools::handle_hideall_unhideall(ctx, msg, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return; + }; + + let mut changed = 0usize; + for channel_id in channels.keys() { + if edit_channel_visibility(ctx, guild_id, *channel_id, None, Some(false)).await { + changed += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnHideAll") + .description(format!("{} salon(s) mis a jour.", changed)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnhideallCommand; @@ -12,14 +37,12 @@ pub static COMMAND_DESCRIPTOR: UnhideallCommand = UnhideallCommand; impl crate::commands::command_contract::CommandSpec for UnhideallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unhideall", - command: "unhideall", + name: "unhideall", category: "admin", params: "aucun", summary: "Affiche tous les salons", description: "Rend visibles tous les salons du serveur.", examples: &["+unhideall"], - alias_source_key: "unhideall", default_aliases: &["uhda"], default_permission: 8, } diff --git a/src/commands/admin/unlock.rs b/src/commands/admin/unlock.rs index 7609294..1ec0a21 100644 --- a/src/commands/admin/unlock.rs +++ b/src/commands/admin/unlock.rs @@ -1,22 +1,47 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_unlock(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_lock_unlock(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let target = args + .first() + .and_then(|raw| parse_channel_id(raw)) + .unwrap_or(msg.channel_id); + + let ok = edit_channel_visibility(ctx, guild_id, target, Some(false), None).await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Unlock") + .description(if ok { + format!("Salon <#{}> mis a jour.", target.get()) + } else { + "Echec de mise a jour du salon.".to_string() + }) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnlockCommand; pub static COMMAND_DESCRIPTOR: UnlockCommand = UnlockCommand; impl crate::commands::command_contract::CommandSpec for UnlockCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unlock", - command: "unlock", + name: "unlock", category: "admin", params: "[salon]", summary: "Ouvre un salon", description: "Deverrouille un salon texte ou vocal.", examples: &["+unlock", "+unlock #general"], - alias_source_key: "unlock", default_aliases: &["ulk"], default_permission: 8, } diff --git a/src/commands/admin/unlockall.rs b/src/commands/admin/unlockall.rs index e8ca248..356dfa7 100644 --- a/src/commands/admin/unlockall.rs +++ b/src/commands/admin/unlockall.rs @@ -1,22 +1,46 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_channel_helpers::edit_channel_visibility; + pub async fn handle_unlockall(ctx: &Context, msg: &Message) { - moderation_tools::handle_lockall_unlockall(ctx, msg, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return; + }; + + let mut changed = 0usize; + for channel_id in channels.keys() { + if edit_channel_visibility(ctx, guild_id, *channel_id, Some(false), None).await { + changed += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnlockAll") + .description(format!("{} salon(s) mis a jour.", changed)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnlockallCommand; pub static COMMAND_DESCRIPTOR: UnlockallCommand = UnlockallCommand; impl crate::commands::command_contract::CommandSpec for UnlockallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unlockall", - command: "unlockall", + name: "unlockall", category: "admin", params: "aucun", summary: "Ouvre tous les salons", description: "Deverrouille tous les salons du serveur.", examples: &["+unlockall"], - alias_source_key: "unlockall", default_aliases: &["ulka"], default_permission: 8, } diff --git a/src/commands/admin/unmassiverole.rs b/src/commands/admin/unmassiverole.rs index de8c0f6..969e545 100644 --- a/src/commands/admin/unmassiverole.rs +++ b/src/commands/admin/unmassiverole.rs @@ -1,10 +1,58 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_role, send_embed, theme_color}; pub async fn handle_unmassiverole(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_massive_role(ctx, msg, args, false).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() { + return; + } + + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + + let Some(target_role) = parse_role(&guild, args[0]) else { + return; + }; + + let filter_role = args.get(1).and_then(|raw| parse_role(&guild, raw)); + + let Ok(members) = guild_id.members(&ctx.http, None, None).await else { + return; + }; + + let mut affected = 0usize; + for member in members { + if let Some(filter) = &filter_role { + if !member.roles.contains(&filter.id) { + continue; + } + } + + if member.remove_role(&ctx.http, target_role.id).await.is_ok() { + affected += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnMassiveRole") + .description(format!( + "{} membres traités pour le rôle <@&{}>.", + affected, + target_role.id.get() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnMassiveRoleCommand; @@ -13,14 +61,12 @@ pub static COMMAND_DESCRIPTOR: UnMassiveRoleCommand = UnMassiveRoleCommand; impl crate::commands::command_contract::CommandSpec for UnMassiveRoleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unmassiverole", - command: "unmassiverole", + name: "unmassiverole", category: "admin", params: " [role_filtre]", summary: "Retire un role en masse", description: "Retire un role a tous les membres ou a ceux qui ont un role filtre.", examples: &["+unmassiverole @VIP", "+unmassiverole @VIP @Membres"], - alias_source_key: "unmassiverole", default_aliases: &["umrole", "umr"], default_permission: 8, } diff --git a/src/commands/admin/unmute.rs b/src/commands/admin/unmute.rs index 5856cc9..6f8d091 100644 --- a/src/commands/admin/unmute.rs +++ b/src/commands/admin/unmute.rs @@ -1,22 +1,64 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{handle_timeout, parse_targets, pool}; + pub async fn handle_unmute(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_unmute(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let affected = handle_timeout(ctx, guild_id, &targets, None).await; + + if let Some(pool) = pool(ctx).await { + let bot_id = ctx.cache.current_user().id; + for uid in &targets { + let _ = sqlx::query( + r#" + UPDATE bot_sanctions + SET active = FALSE + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND active = TRUE AND kind IN ('mute','tempmute'); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(uid.get() as i64) + .execute(&pool) + .await; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnMute") + .description(format!("{} membre(s) unmute.", affected)) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnmuteCommand; pub static COMMAND_DESCRIPTOR: UnmuteCommand = UnmuteCommand; impl crate::commands::command_contract::CommandSpec for UnmuteCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unmute", - command: "unmute", + name: "unmute", category: "admin", params: "<@membre/ID[,..]>", summary: "Retire un mute", description: "Met fin au mute d un ou plusieurs membres.", examples: &["+unmute @User"], - alias_source_key: "unmute", default_aliases: &["um"], default_permission: 8, } diff --git a/src/commands/admin/unmuteall.rs b/src/commands/admin/unmuteall.rs index 3afa3a7..9b62130 100644 --- a/src/commands/admin/unmuteall.rs +++ b/src/commands/admin/unmuteall.rs @@ -1,22 +1,79 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{channel_mute_users, handle_timeout, pool}; + pub async fn handle_unmuteall(ctx: &Context, msg: &Message) { - moderation_tools::handle_unmuteall(ctx, msg).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let rows = sqlx::query_as::<_, (i64, String, Option)>( + r#" + SELECT user_id, kind, channel_id + FROM bot_sanctions + WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute'); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_all(&pool) + .await + .unwrap_or_default(); + + let mut changed = 0usize; + for (uid, kind, channel_id) in rows { + let user_id = UserId::new(uid as u64); + if kind == "mute" || kind == "tempmute" { + changed += handle_timeout(ctx, guild_id, &[user_id], None).await; + } else if let Some(cid) = channel_id { + changed += channel_mute_users(ctx, ChannelId::new(cid as u64), &[user_id], false).await; + } + } + + let _ = sqlx::query( + r#" + UPDATE bot_sanctions + SET active = FALSE + WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute'); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .execute(&pool) + .await; + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnMuteAll") + .description(format!( + "{} operation(s) de unmute/cmute annule(es).", + changed + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnmuteallCommand; pub static COMMAND_DESCRIPTOR: UnmuteallCommand = UnmuteallCommand; impl crate::commands::command_contract::CommandSpec for UnmuteallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unmuteall", - command: "unmuteall", + name: "unmuteall", category: "admin", params: "aucun", summary: "Retire tous les mutes", description: "Supprime tous les mutes en cours.", examples: &["+unmuteall"], - alias_source_key: "unmuteall", default_aliases: &["uma"], default_permission: 8, } diff --git a/src/commands/admin/unowner.rs b/src/commands/admin/unowner.rs index 4389435..c9b63bd 100644 --- a/src/commands/admin/unowner.rs +++ b/src/commands/admin/unowner.rs @@ -74,14 +74,12 @@ pub static COMMAND_DESCRIPTOR: UnownerCommand = UnownerCommand; impl crate::commands::command_contract::CommandSpec for UnownerCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "unowner", - command: "unowner", + name: "unowner", category: "admin", params: "<@membre/ID>", summary: "Retire un owner du bot", description: "Retire un utilisateur de la liste des owners supplementaires du bot.", examples: &["+unowner", "+ur", "+help unowner"], - alias_source_key: "unowner", default_aliases: &["uow"], default_permission: 9, } diff --git a/src/commands/admin/untemprole.rs b/src/commands/admin/untemprole.rs index 5a28168..e9799b9 100644 --- a/src/commands/admin/untemprole.rs +++ b/src/commands/admin/untemprole.rs @@ -1,10 +1,70 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{parse_role, send_embed, theme_color}; +use crate::db::DbPoolKey; pub async fn handle_untemprole(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_untemprole(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.len() < 2 { + return; + } + + let Some(user_id) = parse_user_id(args[0]) else { + return; + }; + + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return; + }; + + let Some(role) = parse_role(&guild, args[1]) else { + return; + }; + + if let Ok(member) = guild_id.member(&ctx.http, user_id).await { + let _ = member.remove_role(&ctx.http, role.id).await; + } + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let bot_id = ctx.cache.current_user().id; + let _ = sqlx::query( + r#" + UPDATE bot_temproles + SET active = FALSE + WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND role_id = $4; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(user_id.get() as i64) + .bind(role.id.get() as i64) + .execute(&pool) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("UnTempRole") + .description(format!( + "Rôle <@&{}> retiré à <@{}>.", + role.id.get(), + user_id.get() + )) + .color(theme_color(ctx).await), + ) + .await; } pub struct UnTempRoleCommand; @@ -13,14 +73,12 @@ pub static COMMAND_DESCRIPTOR: UnTempRoleCommand = UnTempRoleCommand; impl crate::commands::command_contract::CommandSpec for UnTempRoleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "untemprole", - command: "untemprole", + name: "untemprole", category: "admin", params: " ", summary: "Retire un role temporaire", description: "Retire immediatement un role temporaire et desactive son expiration.", examples: &["+untemprole @User @VIP"], - alias_source_key: "untemprole", default_aliases: &["untrole", "deltrole"], default_permission: 8, } diff --git a/src/commands/admin/voicekick.rs b/src/commands/admin/voicekick.rs index 3fa9f97..3f15afc 100644 --- a/src/commands/admin/voicekick.rs +++ b/src/commands/admin/voicekick.rs @@ -1,10 +1,37 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::admin_common::parse_user_id; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_voicekick(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_voicekick(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + if args.is_empty() { + return; + } + + let mut kicked = 0usize; + for raw in args { + if let Some(user_id) = parse_user_id(raw) { + if guild_id.disconnect_member(&ctx.http, user_id).await.is_ok() { + kicked += 1; + } + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("VoiceKick") + .description(format!("{} membres déconnectés.", kicked)) + .color(theme_color(ctx).await), + ) + .await; } pub struct VoiceKickCommand; @@ -13,14 +40,12 @@ pub static COMMAND_DESCRIPTOR: VoiceKickCommand = VoiceKickCommand; impl crate::commands::command_contract::CommandSpec for VoiceKickCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "voicekick", - command: "voicekick", + name: "voicekick", category: "admin", params: "", summary: "Deconnecte des membres du vocal", description: "Deconnecte un ou plusieurs membres de leur salon vocal actuel.", examples: &["+voicekick @User", "+voicekick @U1 @U2"], - alias_source_key: "voicekick", default_aliases: &["vk", "vdisconnect"], default_permission: 8, } diff --git a/src/commands/admin/voicelog.rs b/src/commands/admin/voicelog.rs index 79d7028..4491d7a 100644 --- a/src/commands/admin/voicelog.rs +++ b/src/commands/admin/voicelog.rs @@ -1,9 +1,55 @@ -use crate::commands::logs_service; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::logs_command_helpers::{parse_target_channel, set_log_channel}; + pub async fn handle_voicelog(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_log_toggle(ctx, msg, args, "voice", "VoiceLog").await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(action) = args.first().map(|s| s.to_lowercase()) else { + let embed = CreateEmbed::new() + .title("VoiceLog") + .description("Usage: +voicelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + match action.as_str() { + "on" => { + let channel = parse_target_channel(msg, args, 1); + set_log_channel(ctx, guild_id, "voice", channel, true).await; + let embed = CreateEmbed::new() + .title("VoiceLog") + .description(format!( + "Active dans {}.", + channel + .map(|c| format!("<#{}>", c.get())) + .unwrap_or_else(|| "ce salon".to_string()) + )) + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + "off" => { + set_log_channel(ctx, guild_id, "voice", None, false).await; + let embed = CreateEmbed::new() + .title("VoiceLog") + .description("Desactive.") + .color(theme_color(ctx).await); + send_embed(ctx, msg, embed).await; + } + _ => { + let embed = CreateEmbed::new() + .title("VoiceLog") + .description("Usage: +voicelog ") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + } + } } pub struct VoicelogCommand; @@ -12,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: VoicelogCommand = VoicelogCommand; impl crate::commands::command_contract::CommandSpec for VoicelogCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "voicelog", - command: "voicelog", + name: "voicelog", category: "admin", params: "", summary: "Active les logs vocaux", description: "Active ou desactive les logs de l activite vocale.", examples: &["+voicelog on #logs", "+voicelog off"], - alias_source_key: "voicelog", default_aliases: &["vlog"], default_permission: 8, } diff --git a/src/commands/admin/voicemove.rs b/src/commands/admin/voicemove.rs index 41e5249..f5f3104 100644 --- a/src/commands/admin/voicemove.rs +++ b/src/commands/admin/voicemove.rs @@ -1,10 +1,62 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; pub async fn handle_voicemove(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_voicemove(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.len() < 2 { + return; + } + + let Some(from_channel) = parse_channel_id(args[0]) else { + return; + }; + let Some(to_channel) = parse_channel_id(args[1]) else { + return; + }; + + let user_ids = { + let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { + return; + }; + + guild + .voice_states + .iter() + .filter_map(|(uid, state)| { + if state.channel_id == Some(from_channel) { + Some(*uid) + } else { + None + } + }) + .collect::>() + }; + + let mut moved = 0usize; + for user_id in user_ids { + if guild_id + .move_member(&ctx.http, user_id, to_channel) + .await + .is_ok() + { + moved += 1; + } + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("VoiceMove") + .description(format!("{} membres déplacés.", moved)) + .color(theme_color(ctx).await), + ) + .await; } pub struct VoiceMoveCommand; @@ -13,14 +65,12 @@ pub static COMMAND_DESCRIPTOR: VoiceMoveCommand = VoiceMoveCommand; impl crate::commands::command_contract::CommandSpec for VoiceMoveCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "voicemove", - command: "voicemove", + name: "voicemove", category: "admin", params: " ", summary: "Deplace les membres vocaux", description: "Deplace tous les membres d'un salon vocal vers un autre salon.", examples: &["+voicemove #General #Event"], - alias_source_key: "voicemove", default_aliases: &["vmove", "vmoveall"], default_permission: 8, } diff --git a/src/commands/admin/warn.rs b/src/commands/admin/warn.rs index effadee..4a8fd38 100644 --- a/src/commands/admin/warn.rs +++ b/src/commands/admin/warn.rs @@ -1,22 +1,64 @@ -use crate::commands::moderation_tools; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color}; +use crate::commands::moderation_sanction_helpers::{add_sanction, parse_targets}; + pub async fn handle_warn(ctx: &Context, msg: &Message, args: &[&str]) { - moderation_tools::handle_warn(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + if args.is_empty() { + return; + } + + let targets = parse_targets(args[0]).await; + if targets.is_empty() { + return; + } + + let reason = if args.len() > 1 { + args[1..].join(" ") + } else { + "Aucune raison".to_string() + }; + + for uid in &targets { + add_sanction( + ctx, + guild_id, + *uid, + msg.author.id, + "warn", + &reason, + None, + None, + ) + .await; + } + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Warn") + .description(format!("{} membre(s) warn.", targets.len())) + .color(theme_color(ctx).await), + ) + .await; } pub struct WarnCommand; pub static COMMAND_DESCRIPTOR: WarnCommand = WarnCommand; impl crate::commands::command_contract::CommandSpec for WarnCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "warn", - command: "warn", + name: "warn", category: "admin", params: "<@membre/ID[,..]> [raison]", summary: "Donne un warn", description: "Ajoute un warn a un ou plusieurs membres.", examples: &["+warn @User spam"], - alias_source_key: "warn", default_aliases: &["avert"], default_permission: 8, } diff --git a/src/commands/advanced_tools.rs b/src/commands/advanced_tools.rs index ff7ad05..3b85805 100644 --- a/src/commands/advanced_tools.rs +++ b/src/commands/advanced_tools.rs @@ -8,14 +8,13 @@ use serde::{Deserialize, Serialize}; use serenity::builder::{ CreateActionRow, CreateButton, CreateChannel, CreateEmbed, CreateEmbedFooter, CreateInputText, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, CreateModal, - EditChannel, EditMember, EditMessage, EditRole, + EditMember, EditMessage, EditRole, }; use serenity::model::application::{ActionRowComponent, InputTextStyle}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::admin_common::parse_user_id; -use crate::commands::common::{parse_channel_id, parse_role, send_embed, theme_color}; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; use crate::db::DbPoolKey; static MAINTENANCE_TICK: OnceLock> = OnceLock::new(); @@ -33,10 +32,6 @@ const ADV_CHOOSE_MODAL: &str = "adv:choose:modal"; const ADV_EMBED_MODAL: &str = "adv:embed:modal"; const ADV_LOADING_MODAL: &str = "adv:loading:modal"; -fn adv_component_id(action: &str, owner_id: UserId) -> String { - format!("{}:{}", action, owner_id.get()) -} - fn parse_owner_component_id(custom_id: &str) -> Option<(&str, u64)> { let mut parts = custom_id.rsplitn(2, ':'); let owner = parts.next()?.parse::().ok()?; @@ -184,6 +179,10 @@ fn parse_backup_kind(input: &str) -> Option<&'static str> { } } +pub fn backup_kind_from_input(input: &str) -> Option<&'static str> { + parse_backup_kind(input) +} + fn channel_kind_to_str(kind: ChannelType) -> String { match kind { ChannelType::Text => "text", @@ -553,6 +552,15 @@ async fn create_backup_internal( Ok(()) } +pub async fn backup_create( + ctx: &Context, + guild_id: GuildId, + kind: &str, + name: &str, +) -> Result<(), String> { + create_backup_internal(ctx, guild_id, kind, name).await +} + async fn restore_emoji_backup( ctx: &Context, guild_id: GuildId, @@ -709,337 +717,6 @@ async fn restore_server_backup( Ok((created_roles, created_channels, restored_members)) } -pub async fn handle_giveaway(ctx: &Context, msg: &Message, _args: &[&str]) { - let embed = CreateEmbed::new() - .title("Giveaway") - .description("Utilise les boutons pour créer ou terminer un giveaway via modal.") - .color(theme_color(ctx).await) - .footer(CreateEmbedFooter::new("UI avancée: Components + Modal")); - - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_GIVEAWAY_OPEN_MODAL, msg.author.id)) - .label("Créer") - .emoji('🎉') - .style(ButtonStyle::Success), - CreateButton::new(adv_component_id(ADV_GIVEAWAY_END_MODAL, msg.author.id)) - .label("Terminer") - .emoji('🛑') - .style(ButtonStyle::Danger), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; -} - -async fn handle_end_giveaway(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(raw_id) = args.get(1) else { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Erreur") - .description("Usage: +end giveaway ") - .color(0xED4245), - ) - .await; - return; - }; - - let Ok(message_id) = raw_id.parse::() else { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Erreur") - .description("ID de message invalide.") - .color(0xED4245), - ) - .await; - return; - }; - - let _ = msg - .channel_id - .edit_message( - &ctx.http, - MessageId::new(message_id), - EditMessage::new().content("🎉 Giveaway terminé manuellement."), - ) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Giveaway") - .description(format!("Giveaway `{}` terminé.", message_id)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_reroll(ctx: &Context, msg: &Message, _args: &[&str]) { - let Some(referenced) = msg.referenced_message.as_ref() else { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Reroll") - .description("Réponds à un message giveaway pour reroll.") - .color(0xED4245), - ) - .await; - return; - }; - - let mut candidates = referenced.mentions.iter().map(|u| u.id).collect::>(); - candidates.sort_by_key(|u| u.get()); - candidates.dedup(); - - if candidates.is_empty() { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Reroll") - .description("Aucun participant détecté.") - .color(0xED4245), - ) - .await; - return; - } - - let winner = candidates - .choose(&mut rand::thread_rng()) - .copied() - .unwrap_or(candidates[0]); - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Reroll") - .description(format!("Nouveau gagnant: <@{}>", winner.get())) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_choose(ctx: &Context, msg: &Message, args: &[&str]) { - if args.is_empty() { - let embed = CreateEmbed::new() - .title("Choose") - .description("Ouvre un modal pour saisir les options (séparées par `|`).") - .color(theme_color(ctx).await); - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_CHOOSE_MODAL, msg.author.id)) - .label("Saisir les options") - .style(ButtonStyle::Primary), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; - return; - } - - let merged = args.join(" "); - let mut options = merged - .split('|') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect::>(); - - if options.len() < 2 { - options = args.iter().map(|s| (*s).to_string()).collect(); - } - - if options.len() < 2 { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Choose") - .description("Donne au moins 2 options.") - .color(0xED4245), - ) - .await; - return; - } - - let pick = options - .choose(&mut rand::thread_rng()) - .cloned() - .unwrap_or_else(|| options[0].clone()); - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Tirage") - .description(format!("Résultat: **{}**", pick)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_embed_builder(ctx: &Context, msg: &Message, _args: &[&str]) { - let embed = CreateEmbed::new() - .title("Générateur d'embed") - .description("Utilise le bouton pour construire un embed via modal.") - .color(theme_color(ctx).await); - - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_EMBED_MODAL, msg.author.id)) - .label("Créer un embed") - .style(ButtonStyle::Primary), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; -} - -pub async fn handle_backup(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.is_empty() { - let embed = CreateEmbed::new() - .title("Backup") - .description("Choisis une action. Les boutons ouvrent un modal.") - .color(theme_color(ctx).await); - - let components = vec![ - CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_BACKUP_CREATE_MODAL, msg.author.id)) - .label("Créer") - .style(ButtonStyle::Success), - CreateButton::new(adv_component_id(ADV_BACKUP_LIST_MODAL, msg.author.id)) - .label("Lister") - .style(ButtonStyle::Primary), - ]), - CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_BACKUP_LOAD_MODAL, msg.author.id)) - .label("Charger") - .style(ButtonStyle::Primary), - CreateButton::new(adv_component_id(ADV_BACKUP_DELETE_MODAL, msg.author.id)) - .label("Supprimer") - .style(ButtonStyle::Danger), - ]), - ]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; - return; - } - - let action_or_kind = args[0].to_lowercase(); - - if action_or_kind == "list" { - let Some(kind_raw) = args.get(1) else { - return; - }; - let Some(kind) = parse_backup_kind(kind_raw) else { - return; - }; - handle_backup_list(ctx, msg, guild_id, kind).await; - return; - } - - if action_or_kind == "delete" { - let (Some(kind_raw), Some(name)) = (args.get(1), args.get(2)) else { - return; - }; - let Some(kind) = parse_backup_kind(kind_raw) else { - return; - }; - handle_backup_delete(ctx, msg, guild_id, kind, name).await; - return; - } - - if action_or_kind == "load" { - let (Some(kind_raw), Some(name)) = (args.get(1), args.get(2)) else { - return; - }; - let Some(kind) = parse_backup_kind(kind_raw) else { - return; - }; - handle_backup_load(ctx, msg, guild_id, kind, name).await; - return; - } - - let Some(kind) = parse_backup_kind(&action_or_kind) else { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Backup") - .description("Type invalide: utilise serveur ou emoji.") - .color(0xED4245), - ) - .await; - return; - }; - - let Some(name) = args.get(1) else { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Backup") - .description("Tu dois préciser un nom de backup.") - .color(0xED4245), - ) - .await; - return; - }; - - match create_backup_internal(ctx, guild_id, kind, name).await { - Ok(()) => { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Backup") - .description(format!("Backup `{}` de type `{}` créée.", name, kind)) - .color(theme_color(ctx).await), - ) - .await; - } - Err(err) => { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Backup") - .description(format!("Erreur: {}", err)) - .color(0xED4245), - ) - .await; - } - } -} - async fn handle_backup_list(ctx: &Context, msg: &Message, guild_id: GuildId, kind: &str) { let Some(pool) = pool(ctx).await else { return; @@ -1081,6 +758,10 @@ async fn handle_backup_list(ctx: &Context, msg: &Message, guild_id: GuildId, kin .await; } +pub async fn backup_list(ctx: &Context, msg: &Message, guild_id: GuildId, kind: &str) { + handle_backup_list(ctx, msg, guild_id, kind).await; +} + async fn handle_backup_delete( ctx: &Context, msg: &Message, @@ -1126,6 +807,16 @@ async fn handle_backup_delete( .await; } +pub async fn backup_delete( + ctx: &Context, + msg: &Message, + guild_id: GuildId, + kind: &str, + name: &str, +) { + handle_backup_delete(ctx, msg, guild_id, kind, name).await; +} + async fn handle_backup_load( ctx: &Context, msg: &Message, @@ -1201,910 +892,8 @@ async fn handle_backup_load( .await; } -pub async fn handle_autobackup(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - let Some(kind_raw) = args.first() else { - return; - }; - let Some(days_raw) = args.get(1) else { - return; - }; - - let Some(kind) = parse_backup_kind(kind_raw) else { - return; - }; - - let Ok(days) = days_raw.parse::() else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - - let bot_id = ctx.cache.current_user().id; - let _ = sqlx::query( - r#" - INSERT INTO bot_autobackups (bot_id, guild_id, kind, interval_days, next_run_at) - VALUES ($1, $2, $3, $4, NOW() + make_interval(days => $4)) - ON CONFLICT (bot_id, guild_id, kind) - DO UPDATE SET interval_days = EXCLUDED.interval_days, - next_run_at = NOW() + make_interval(days => EXCLUDED.interval_days); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(kind) - .bind(days.max(1)) - .execute(&pool) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("AutoBackup") - .description(format!( - "Auto-backup `{}` configurée toutes les {} jours.", - kind, - days.max(1) - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_loading(ctx: &Context, msg: &Message, args: &[&str]) { - if args.len() < 2 { - let embed = CreateEmbed::new() - .title("Loading") - .description("Ouvre un modal pour saisir la durée et le message.") - .color(theme_color(ctx).await); - - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_LOADING_MODAL, msg.author.id)) - .label("Configurer") - .style(ButtonStyle::Primary), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; - return; - } - - let Some(duration) = duration_from_input(args[0]) else { - return; - }; - - let total_secs = duration.as_secs().clamp(1, 120); - let text = args[1..].join(" "); - - let mut sent = match msg - .channel_id - .send_message(&ctx.http, CreateMessage::new().content("[----------] 0%")) - .await - { - Ok(m) => m, - Err(_) => return, - }; - - for i in 0..=10_u64 { - let done = "#".repeat(i as usize); - let todo = "-".repeat((10 - i) as usize); - let percent = i * 10; - - let _ = sent - .edit( - &ctx.http, - EditMessage::new().content(format!("{} [{}{}] {}%", text, done, todo, percent)), - ) - .await; - - if i < 10 { - tokio::time::sleep(Duration::from_secs((total_secs / 10).max(1))).await; - } - } -} - -fn emoji_url_from_source(msg: &Message, source: &str) -> String { - if source.starts_with("http://") || source.starts_with("https://") { - return source.to_string(); - } - - if source.starts_with("<:") || source.starts_with("'); - let parts = cleaned.split(':').collect::>(); - if parts.len() == 3 { - let animated = parts[0] == "a"; - return format!( - "https://cdn.discordapp.com/emojis/{}.{}", - parts[2], - if animated { "gif" } else { "png" } - ); - } - } - - if let Some(att) = msg.attachments.first() { - return att.url.clone(); - } - - String::new() -} - -pub async fn handle_create_emoji(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.len() < 2 { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Create Emoji") - .description("Usage: +create ") - .color(0xED4245), - ) - .await; - return; - } - - let image_url = emoji_url_from_source(msg, args[0]); - if image_url.is_empty() { - return; - } - - let response = match reqwest::get(&image_url).await { - Ok(r) => r, - Err(_) => return, - }; - let bytes = match response.bytes().await { - Ok(b) => b, - Err(_) => return, - }; - - let data_uri = format!("data:image/png;base64,{}", { - use base64::Engine; - base64::engine::general_purpose::STANDARD.encode(bytes) - }); - - let result = guild_id.create_emoji(&ctx.http, args[1], &data_uri).await; - - let embed = if let Ok(emoji) = result { - CreateEmbed::new() - .title("Emoji") - .description(format!("Emoji créé: {}", emoji)) - .color(theme_color(ctx).await) - } else { - CreateEmbed::new() - .title("Emoji") - .description("Impossible de créer l'emoji.") - .color(0xED4245) - }; - - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_new_sticker(ctx: &Context, msg: &Message, _args: &[&str]) { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("NewSticker") - .description("Création de sticker disponible prochainement (API sticker V2).") - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_massive_role(ctx: &Context, msg: &Message, args: &[&str], add: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.is_empty() { - return; - } - - let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { - return; - }; - - let Some(target_role) = parse_role(&guild, args[0]) else { - return; - }; - - let filter_role = args.get(1).and_then(|raw| parse_role(&guild, raw)); - let members = guild_id - .members(&ctx.http, None, None) - .await - .unwrap_or_default(); - - let mut affected = 0usize; - for member in members { - if let Some(filter) = &filter_role { - if !member.roles.contains(&filter.id) { - continue; - } - } - - let result = if add { - member.add_role(&ctx.http, target_role.id).await - } else { - member.remove_role(&ctx.http, target_role.id).await - }; - - if result.is_ok() { - affected += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if add { "MassiveRole" } else { "UnMassiveRole" }) - .description(format!( - "{} membres traités pour le rôle <@&{}>.", - affected, - target_role.id.get() - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_voicemove(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.len() < 2 { - return; - } - - let Some(from_channel) = parse_channel_id(args[0]) else { - return; - }; - let Some(to_channel) = parse_channel_id(args[1]) else { - return; - }; - - let user_ids = { - let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { - return; - }; - - guild - .voice_states - .iter() - .filter_map(|(uid, state)| { - if state.channel_id == Some(from_channel) { - Some(*uid) - } else { - None - } - }) - .collect::>() - }; - - let mut moved = 0usize; - for user_id in user_ids { - if guild_id - .move_member(&ctx.http, user_id, to_channel) - .await - .is_ok() - { - moved += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("VoiceMove") - .description(format!("{} membres déplacés.", moved)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_voicekick(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.is_empty() { - return; - } - - let mut kicked = 0usize; - for raw in args { - if let Some(user_id) = parse_user_id(raw) { - if guild_id.disconnect_member(&ctx.http, user_id).await.is_ok() { - kicked += 1; - } - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("VoiceKick") - .description(format!("{} membres déconnectés.", kicked)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_cleanup_voice(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(channel_raw) = args.first() else { - return; - }; - let Some(channel_id) = parse_channel_id(channel_raw) else { - return; - }; - - let user_ids = { - let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { - return; - }; - - guild - .voice_states - .iter() - .filter_map(|(uid, state)| { - if state.channel_id == Some(channel_id) { - Some(*uid) - } else { - None - } - }) - .collect::>() - }; - - let mut kicked = 0usize; - for user_id in user_ids { - if guild_id.disconnect_member(&ctx.http, user_id).await.is_ok() { - kicked += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Cleanup") - .description(format!("{} utilisateurs déconnectés.", kicked)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_bringall(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let target_channel = if let Some(raw) = args.first() { - parse_channel_id(raw) - } else { - let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { - return; - }; - guild - .voice_states - .get(&msg.author.id) - .and_then(|v| v.channel_id) - }; - - let Some(target_channel) = target_channel else { - return; - }; - - let user_ids = { - let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { - return; - }; - - guild - .voice_states - .iter() - .filter_map(|(uid, state)| state.channel_id.map(|_| *uid)) - .collect::>() - }; - - let mut moved = 0usize; - for user_id in user_ids { - if guild_id - .move_member(&ctx.http, user_id, target_channel) - .await - .is_ok() - { - moved += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("BringAll") - .description(format!("{} membres déplacés.", moved)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_renew(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let channel_id = args - .first() - .and_then(|raw| parse_channel_id(raw)) - .unwrap_or(msg.channel_id); - - let Ok(channel) = channel_id.to_channel(&ctx.http).await else { - return; - }; - let Channel::Guild(text_channel) = channel else { - return; - }; - - if text_channel.kind != ChannelType::Text && text_channel.kind != ChannelType::News { - return; - } - - let parent_id = text_channel.parent_id; - let topic = text_channel.topic.clone(); - let nsfw = text_channel.nsfw; - let slowmode = text_channel.rate_limit_per_user; - let name = text_channel.name.clone(); - - let _ = text_channel.delete(&ctx.http).await; - - let mut builder = CreateChannel::new(name) - .kind(ChannelType::Text) - .nsfw(nsfw) - .rate_limit_per_user(slowmode.unwrap_or(0)); - - if let Some(parent) = parent_id { - builder = builder.category(parent); - } - if let Some(topic) = topic { - builder = builder.topic(topic); - } - - let _ = guild_id.create_channel(&ctx.http, builder).await; -} - -pub async fn handle_unbanall(ctx: &Context, msg: &Message, _args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let bans = guild_id - .bans(&ctx.http, None, None) - .await - .unwrap_or_default(); - - let mut unbanned = 0usize; - for ban in bans { - if guild_id.unban(&ctx.http, ban.user.id).await.is_ok() { - unbanned += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnbanAll") - .description(format!("{} bannissements retirés.", unbanned)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_temprole(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.len() < 3 { - return; - } - - let Some(user_id) = parse_user_id(args[0]) else { - return; - }; - - let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { - return; - }; - - let Some(role) = parse_role(&guild, args[1]) else { - return; - }; - - let Some(duration) = duration_from_input(args[2]) else { - return; - }; - - let expires_at = Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64); - - if let Ok(member) = guild_id.member(&ctx.http, user_id).await { - let _ = member.add_role(&ctx.http, role.id).await; - } - - if let Some(pool) = pool(ctx).await { - let bot_id = ctx.cache.current_user().id; - let _ = sqlx::query( - r#" - INSERT INTO bot_temproles (bot_id, guild_id, user_id, role_id, expires_at, active, added_by) - VALUES ($1, $2, $3, $4, $5, TRUE, $6) - ON CONFLICT (bot_id, guild_id, user_id, role_id) - DO UPDATE SET expires_at = EXCLUDED.expires_at, active = TRUE, added_by = EXCLUDED.added_by; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(user_id.get() as i64) - .bind(role.id.get() as i64) - .bind(expires_at) - .bind(msg.author.id.get() as i64) - .execute(&pool) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("TempRole") - .description(format!( - "Rôle <@&{}> ajouté à <@{}> jusqu'à .", - role.id.get(), - user_id.get(), - expires_at.timestamp() - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_untemprole(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args.len() < 2 { - return; - } - - let Some(user_id) = parse_user_id(args[0]) else { - return; - }; - - let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { - return; - }; - - let Some(role) = parse_role(&guild, args[1]) else { - return; - }; - - if let Ok(member) = guild_id.member(&ctx.http, user_id).await { - let _ = member.remove_role(&ctx.http, role.id).await; - } - - if let Some(pool) = pool(ctx).await { - let bot_id = ctx.cache.current_user().id; - let _ = sqlx::query( - r#" - UPDATE bot_temproles - SET active = FALSE - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND role_id = $4; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(user_id.get() as i64) - .bind(role.id.get() as i64) - .execute(&pool) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnTempRole") - .description(format!( - "Rôle <@&{}> retiré à <@{}>.", - role.id.get(), - user_id.get() - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_sync(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(scope) = args.first() else { - return; - }; - - let Ok(channels) = guild_id.channels(&ctx.http).await else { - return; - }; - - let ids_to_sync = if scope.eq_ignore_ascii_case("all") { - channels.keys().copied().collect::>() - } else if let Some(channel_id) = parse_channel_id(scope) { - if let Some(target) = channels.get(&channel_id) { - if target.kind == ChannelType::Category { - channels - .values() - .filter(|ch| ch.parent_id == Some(channel_id)) - .map(|ch| ch.id) - .collect::>() - } else { - vec![channel_id] - } - } else { - vec![channel_id] - } - } else { - Vec::new() - }; - - let mut synced = 0usize; - for channel_id in ids_to_sync { - let Some(channel) = channels.get(&channel_id) else { - continue; - }; - let Some(parent_id) = channel.parent_id else { - continue; - }; - let Some(parent) = channels.get(&parent_id) else { - continue; - }; - - if channel_id - .edit( - &ctx.http, - EditChannel::new().permissions(parent.permission_overwrites.clone()), - ) - .await - .is_ok() - { - synced += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Sync") - .description(format!("{} salons synchronisés.", synced)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_button(ctx: &Context, msg: &Message, args: &[&str]) { - if args.len() < 2 { - return; - } - - let action = args[0].to_lowercase(); - let link = args[1]; - - if action == "add" { - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new() - .content("Bouton personnalisé") - .components(vec![CreateActionRow::Buttons(vec![ - CreateButton::new_link(link).label("Ouvrir"), - ])]), - ) - .await; - } else if action == "del" { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Button") - .description("Suppression: supprime simplement le message contenant le bouton.") - .color(theme_color(ctx).await), - ) - .await; - } -} - -pub async fn handle_autoreact(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(action) = args.first().map(|s| s.to_lowercase()) else { - let embed = CreateEmbed::new() - .title("AutoReact") - .description("Utilise les boutons pour ajouter/supprimer/lister via UI.") - .color(theme_color(ctx).await); - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_AUTOREACT_ADD_MODAL, msg.author.id)) - .label("Ajouter") - .style(ButtonStyle::Success), - CreateButton::new(adv_component_id(ADV_AUTOREACT_DEL_MODAL, msg.author.id)) - .label("Supprimer") - .style(ButtonStyle::Danger), - CreateButton::new(adv_component_id(ADV_AUTOREACT_LIST, msg.author.id)) - .label("Lister") - .style(ButtonStyle::Primary), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - - let bot_id = ctx.cache.current_user().id; - - if action == "list" { - let rows = sqlx::query_as::<_, (i64, String)>( - r#" - SELECT channel_id, emoji - FROM bot_autoreacts - WHERE bot_id = $1 AND guild_id = $2 - ORDER BY channel_id ASC, emoji ASC; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .fetch_all(&pool) - .await - .unwrap_or_default(); - - let desc = if rows.is_empty() { - "Aucun autoreact configuré.".to_string() - } else { - rows.into_iter() - .map(|(channel_id, emoji)| format!("- <#{}> -> {}", channel_id, emoji)) - .collect::>() - .join("\n") - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("AutoReact") - .description(desc) - .color(theme_color(ctx).await), - ) - .await; - return; - } - - if args.len() < 3 { - return; - } - - let Some(channel_id) = parse_channel_id(args[1]) else { - return; - }; - let emoji = args[2]; - - if action == "add" { - let _ = sqlx::query( - r#" - INSERT INTO bot_autoreacts (bot_id, guild_id, channel_id, emoji) - VALUES ($1, $2, $3, $4) - ON CONFLICT (bot_id, guild_id, channel_id, emoji) DO NOTHING; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(channel_id.get() as i64) - .bind(emoji) - .execute(&pool) - .await; - } else if action == "del" { - let _ = sqlx::query( - r#" - DELETE FROM bot_autoreacts - WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3 AND emoji = $4; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(channel_id.get() as i64) - .bind(emoji) - .execute(&pool) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("AutoReact") - .description("Configuration mise à jour.") - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_end(ctx: &Context, msg: &Message, args: &[&str]) { - if args.is_empty() { - let embed = CreateEmbed::new() - .title("End") - .description("Utilise le bouton pour terminer un giveaway via modal.") - .color(theme_color(ctx).await); - let components = vec![CreateActionRow::Buttons(vec![ - CreateButton::new(adv_component_id(ADV_GIVEAWAY_END_MODAL, msg.author.id)) - .label("Terminer un giveaway") - .style(ButtonStyle::Danger), - ])]; - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; - return; - } - - if args - .first() - .map(|v| v.eq_ignore_ascii_case("giveaway")) - .unwrap_or(false) - { - handle_end_giveaway(ctx, msg, args).await; - } -} - -pub async fn handle_create(ctx: &Context, msg: &Message, args: &[&str]) { - handle_create_emoji(ctx, msg, args).await; -} - -pub async fn handle_cleanup(ctx: &Context, msg: &Message, args: &[&str]) { - handle_cleanup_voice(ctx, msg, args).await; +pub async fn backup_load(ctx: &Context, msg: &Message, guild_id: GuildId, kind: &str, name: &str) { + handle_backup_load(ctx, msg, guild_id, kind, name).await; } pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool { diff --git a/src/commands/botconfig_common.rs b/src/commands/botconfig_common.rs index f1efd52..f521b6a 100644 --- a/src/commands/botconfig_common.rs +++ b/src/commands/botconfig_common.rs @@ -1,11 +1,3 @@ -use serenity::builder::CreateEmbed; -use serenity::model::prelude::*; -use serenity::prelude::*; - -use crate::activity::{RotatingActivityKind, parse_status, start_rotation, stop_rotation}; -use crate::commands::common::send_embed; -use crate::db::{DbPoolKey, set_bot_status}; - pub fn parse_color(value: &str) -> Option { let v = value.trim().to_lowercase(); match v.as_str() { @@ -24,143 +16,3 @@ pub fn parse_color(value: &str) -> Option { } } } - -pub async fn save_status_if_db(ctx: &Context, status: &str) { - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - if let Some(pool) = pool { - let _ = set_bot_status(&pool, bot_id, status).await; - } -} - -pub async fn handle_activity(ctx: &Context, msg: &Message, command: &str, args: &[&str]) { - if args.is_empty() { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `+playto|+listen|+watch|+compet|+stream `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let Some(kind) = RotatingActivityKind::from_command(command) else { - return; - }; - - let joined = args.join(" "); - let messages: Vec = joined - .split(",,") - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .collect(); - - if messages.is_empty() { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Aucun message d'activité valide.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let bot_id = ctx.cache.current_user().id; - - let status = { - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - if let Some(pool) = pool { - if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { - parse_status(&saved) - } else { - OnlineStatus::Online - } - } else { - OnlineStatus::Online - } - }; - - start_rotation(ctx, kind, messages.clone(), status).await; - - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - if let Some(pool) = pool { - let _ = - crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; - } - - let embed = CreateEmbed::new() - .title("Activité mise à jour") - .description(format!("{} message(s) configuré(s).", messages.len())) - .field( - "Rotation", - "Les textes alternent toutes les 30 secondes.", - false, - ) - .color(0x57F287); - - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_remove_activity(ctx: &Context, msg: &Message) { - stop_rotation(ctx).await; - ctx.set_activity(None); - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - if let Some(pool) = pool { - let _ = crate::db::clear_bot_activity(&pool, bot_id).await; - } - - let embed = CreateEmbed::new() - .title("Activité supprimée") - .description("L'activité du bot a été retirée.") - .color(0x57F287); - - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_status(ctx: &Context, msg: &Message, command: &str) { - let status_name = match command { - "+online" => { - ctx.online(); - "online" - } - "+idle" => { - ctx.idle(); - "idle" - } - "+dnd" => { - ctx.dnd(); - "dnd" - } - "+invisible" => { - ctx.invisible(); - "invisible" - } - _ => return, - }; - - save_status_if_db(ctx, status_name).await; - - let embed = CreateEmbed::new() - .title("Statut mis à jour") - .description(format!("Nouveau statut: {}", status_name)) - .color(0x57F287); - - send_embed(ctx, msg, embed).await; -} diff --git a/src/commands/command_contract.rs b/src/commands/command_contract.rs index 697baf7..422ce02 100644 --- a/src/commands/command_contract.rs +++ b/src/commands/command_contract.rs @@ -1,14 +1,12 @@ #[derive(Clone, Copy)] pub struct CommandMetadata { - pub key: &'static str, - pub command: &'static str, + pub name: &'static str, pub category: &'static str, pub default_permission: u8, pub params: &'static str, pub summary: &'static str, pub description: &'static str, pub examples: &'static [&'static str], - pub alias_source_key: &'static str, pub default_aliases: &'static [&'static str], } diff --git a/src/commands/general/alladmins.rs b/src/commands/general/alladmins.rs index 7576c61..df4cd1c 100644 --- a/src/commands/general/alladmins.rs +++ b/src/commands/general/alladmins.rs @@ -136,14 +136,12 @@ pub static COMMAND_DESCRIPTOR: AlladminsCommand = AlladminsCommand; impl crate::commands::command_contract::CommandSpec for AlladminsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "alladmins", - command: "alladmins", + name: "alladmins", category: "general", params: "aucun", summary: "Liste les administrateurs du serveur", description: "Affiche les membres qui possedent des droits administrateur sur le serveur.", examples: &["+alladmins", "+as", "+help alladmins"], - alias_source_key: "alladmins", default_aliases: &["aad"], default_permission: 0, } diff --git a/src/commands/general/allbots.rs b/src/commands/general/allbots.rs index 9884d7f..7c9982a 100644 --- a/src/commands/general/allbots.rs +++ b/src/commands/general/allbots.rs @@ -119,14 +119,12 @@ pub static COMMAND_DESCRIPTOR: AllbotsCommand = AllbotsCommand; impl crate::commands::command_contract::CommandSpec for AllbotsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "allbots", - command: "allbots", + name: "allbots", category: "general", params: "aucun", summary: "Liste tous les bots du serveur", description: "Affiche la liste des membres bots presents sur le serveur courant.", examples: &["+allbots", "+as", "+help allbots"], - alias_source_key: "allbots", default_aliases: &["abt"], default_permission: 0, } diff --git a/src/commands/general/banner.rs b/src/commands/general/banner.rs index 6f2000a..37accb5 100644 --- a/src/commands/general/banner.rs +++ b/src/commands/general/banner.rs @@ -64,14 +64,12 @@ pub static COMMAND_DESCRIPTOR: BannerCommand = BannerCommand; impl crate::commands::command_contract::CommandSpec for BannerCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "banner", - command: "banner", + name: "banner", category: "general", params: "<@membre/ID>", summary: "Affiche la banniere utilisateur", description: "Affiche la banniere de profil dun utilisateur cible ou de lauteur.", examples: &["+banner", "+br", "+help banner"], - alias_source_key: "banner", default_aliases: &["bnr"], default_permission: 0, } diff --git a/src/commands/general/boosters.rs b/src/commands/general/boosters.rs index 6738132..8925718 100644 --- a/src/commands/general/boosters.rs +++ b/src/commands/general/boosters.rs @@ -131,14 +131,12 @@ pub static COMMAND_DESCRIPTOR: BoostersCommand = BoostersCommand; impl crate::commands::command_contract::CommandSpec for BoostersCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "boosters", - command: "boosters", + name: "boosters", category: "general", params: "aucun", summary: "Liste les boosters du serveur", description: "Affiche les membres qui boostent actuellement le serveur.", examples: &["+boosters", "+bs", "+help boosters"], - alias_source_key: "boosters", default_aliases: &["bst"], default_permission: 0, } diff --git a/src/commands/general/botadmins.rs b/src/commands/general/botadmins.rs index 2ea04dc..e035600 100644 --- a/src/commands/general/botadmins.rs +++ b/src/commands/general/botadmins.rs @@ -135,14 +135,12 @@ pub static COMMAND_DESCRIPTOR: BotadminsCommand = BotadminsCommand; impl crate::commands::command_contract::CommandSpec for BotadminsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "botadmins", - command: "botadmins", + name: "botadmins", category: "general", params: "aucun", summary: "Liste les admins du bot", description: "Affiche les utilisateurs ayant des droits admin sur le bot.", examples: &["+botadmins", "+bs", "+help botadmins"], - alias_source_key: "botadmins", default_aliases: &["bad"], default_permission: 0, } diff --git a/src/commands/general/calc.rs b/src/commands/general/calc.rs index 9fed8cf..1dc9fff 100644 --- a/src/commands/general/calc.rs +++ b/src/commands/general/calc.rs @@ -94,14 +94,12 @@ pub static COMMAND_DESCRIPTOR: CalcCommand = CalcCommand; impl crate::commands::command_contract::CommandSpec for CalcCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "calc", - command: "calc", + name: "calc", category: "general", params: "", summary: "Calcule une expression", description: "Evalue une expression numerique simple et renvoie le resultat.", examples: &["+calc", "+cc", "+help calc"], - alias_source_key: "calc", default_aliases: &["clc"], default_permission: 0, } diff --git a/src/commands/general/channel.rs b/src/commands/general/channel.rs index ac26077..30c3cf9 100644 --- a/src/commands/general/channel.rs +++ b/src/commands/general/channel.rs @@ -98,14 +98,12 @@ pub static COMMAND_DESCRIPTOR: ChannelCommand = ChannelCommand; impl crate::commands::command_contract::CommandSpec for ChannelCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "channel", - command: "channel", + name: "channel", category: "general", params: "<#salon/ID>", summary: "Affiche les details dun salon", description: "Affiche les informations utiles dun salon texte ou vocal cible.", examples: &["+channel", "+cl", "+help channel"], - alias_source_key: "channel", default_aliases: &["chl"], default_permission: 0, } diff --git a/src/commands/general/choose.rs b/src/commands/general/choose.rs index 6d1a8b7..85ec565 100644 --- a/src/commands/general/choose.rs +++ b/src/commands/general/choose.rs @@ -1,10 +1,70 @@ +use rand::seq::SliceRandom; +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::advanced_tools; +use crate::commands::common::{send_embed, theme_color}; pub async fn handle_choose(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_choose(ctx, msg, args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Choose") + .description("Ouvre un modal pour saisir les options (séparées par `|`).") + .color(theme_color(ctx).await); + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(format!("adv:choose:modal:{}", msg.author.id.get())) + .label("Saisir les options") + .style(serenity::all::ButtonStyle::Primary), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; + return; + } + + let merged = args.join(" "); + let mut options = merged + .split('|') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect::>(); + + if options.len() < 2 { + options = args.iter().map(|s| (*s).to_string()).collect(); + } + + if options.len() < 2 { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Choose") + .description("Donne au moins 2 options.") + .color(0xED4245), + ) + .await; + return; + } + + let pick = options + .choose(&mut rand::thread_rng()) + .cloned() + .unwrap_or_else(|| options[0].clone()); + + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("Tirage") + .description(format!("Résultat: **{}**", pick)) + .color(theme_color(ctx).await), + ) + .await; } pub struct ChooseCommand; @@ -13,14 +73,12 @@ pub static COMMAND_DESCRIPTOR: ChooseCommand = ChooseCommand; impl crate::commands::command_contract::CommandSpec for ChooseCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "choose", - command: "choose", + name: "choose", category: "general", params: "", summary: "Tire une option au hasard", description: "Lance un tirage au sort instantane parmi les options donnees.", examples: &["+choose rouge | bleu | vert"], - alias_source_key: "choose", default_aliases: &["pick", "random"], default_permission: 8, } diff --git a/src/commands/general/emoji.rs b/src/commands/general/emoji.rs index 906a0e7..f131b30 100644 --- a/src/commands/general/emoji.rs +++ b/src/commands/general/emoji.rs @@ -83,14 +83,12 @@ pub static COMMAND_DESCRIPTOR: EmojiCommand = EmojiCommand; impl crate::commands::command_contract::CommandSpec for EmojiCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "emoji", - command: "emoji", + name: "emoji", category: "general", params: "", summary: "Affiche les infos dun emoji", description: "Affiche les details dun emoji fourni.", examples: &["+emoji", "+ei", "+help emoji"], - alias_source_key: "emoji", default_aliases: &["emj"], default_permission: 0, } diff --git a/src/commands/general/help.rs b/src/commands/general/help.rs index 4f468fa..12652d2 100644 --- a/src/commands/general/help.rs +++ b/src/commands/general/help.rs @@ -121,7 +121,7 @@ const HELP_PAGES: &[HelpPage] = &[ fn help_page_for_command( meta: &crate::commands::command_contract::CommandMetadata, ) -> &'static str { - match meta.key { + match meta.name { "modlog" | "messagelog" | "voicelog" | "boostlog" | "rolelog" | "raidlog" | "autoconfiglog" | "nolog" | "join" | "boostembed" | "set_modlogs" | "set_boostembed" | "leave_settings" | "viewlogs" => "logs", @@ -195,11 +195,14 @@ fn help_metadata_lookup_key(input: &str) -> Option<&'static str> { crate::commands::all_command_metadata() .into_iter() .find(|meta| { - meta.key.eq_ignore_ascii_case(&normalized) - || meta.key.eq_ignore_ascii_case(&underscored) - || meta.command.eq_ignore_ascii_case(&normalized) + meta.name.eq_ignore_ascii_case(&normalized) + || meta.name.eq_ignore_ascii_case(&underscored) + || meta + .name + .replace('_', " ") + .eq_ignore_ascii_case(&normalized) }) - .map(|meta| meta.key) + .map(|meta| meta.name) } fn help_page_matches_input(page: &HelpPage, input: &str) -> bool { @@ -277,7 +280,7 @@ async fn aliases_map(ctx: &Context) -> BTreeMap> { for meta in crate::commands::all_command_metadata() { if !meta.default_aliases.is_empty() { - out.entry(meta.alias_source_key.to_string()) + out.entry(meta.name.to_string()) .or_default() .extend(meta.default_aliases.iter().map(|alias| alias.to_string())); } @@ -310,8 +313,8 @@ fn command_doc(key: &str) -> Option { }; Some(CommandDoc { - key: meta.key, - command: meta.command, + key: meta.name, + command: meta.name, default_permission: meta.default_permission, params: meta.params, summary: meta.summary, @@ -321,7 +324,7 @@ fn command_doc(key: &str) -> Option { } else { meta.examples }, - alias_source_key: Some(meta.alias_source_key), + alias_source_key: Some(meta.name), }) } @@ -467,14 +470,14 @@ fn help_page_content( .into_iter() .filter(|meta| help_page_for_command(meta).eq_ignore_ascii_case(page.key)) .collect::>(); - commands.sort_by(|a, b| a.command.to_lowercase().cmp(&b.command.to_lowercase())); + commands.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); let mut lines = Vec::with_capacity(commands.len()); for meta in commands { - let label = meta.command; + let label = meta.name.replace('_', " "); let summary = meta.summary; - let alias_key = meta.alias_source_key; + let alias_key = meta.name; let permission = if perms_enabled { format!(" {}", format_permission_level(meta.default_permission)) } else { @@ -745,9 +748,16 @@ pub async fn handle_help(ctx: &Context, msg: &Message, args: &[&str]) { .join("\n"); let mut embed = CreateEmbed::new() - .title(format!("Aide commande · +{}", doc.command)) + .title(format!( + "Aide commande · +{}", + doc.command.replace('_', " ") + )) .description(doc.description) - .field("Commande", format!("`+{}`", doc.command), false) + .field( + "Commande", + format!("`+{}`", doc.command.replace('_', " ")), + false, + ) .field("Clé ACL", format!("`{}`", doc.key), false) .field("Catégorie", help_page_title_for_command_key(doc.key), false) .field("Alias", alias_text, false) @@ -797,14 +807,12 @@ pub static COMMAND_DESCRIPTOR: HelpCommand = HelpCommand; impl crate::commands::command_contract::CommandSpec for HelpCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "help", - command: "help", + name: "help", category: "general", params: "[commande|page]", summary: "Affiche laide des commandes", description: "Affiche les pages daide du bot ou la fiche detaillee dune commande avec parametres, aliases et exemples.", examples: &["+help", "+hp", "+help help"], - alias_source_key: "help", default_aliases: &["hp"], default_permission: 0, } diff --git a/src/commands/general/loading.rs b/src/commands/general/loading.rs index 6e74fe1..09606a8 100644 --- a/src/commands/general/loading.rs +++ b/src/commands/general/loading.rs @@ -1,10 +1,96 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage, EditMessage}; use serenity::model::prelude::*; use serenity::prelude::*; +use std::time::Duration; -use crate::commands::advanced_tools; +use crate::commands::common::theme_color; + +fn duration_from_input(input: &str) -> Option { + let raw = input.trim().to_lowercase(); + if raw.is_empty() { + return None; + } + + let mut number = String::new(); + let mut suffix = String::new(); + for ch in raw.chars() { + if ch.is_ascii_digit() { + if !suffix.is_empty() { + return None; + } + number.push(ch); + } else if !ch.is_whitespace() { + suffix.push(ch); + } + } + + let value = number.parse::().ok()?; + let secs = match suffix.as_str() { + "s" | "sec" | "secs" | "seconde" | "secondes" => value, + "m" | "min" | "mins" | "minute" | "minutes" => value * 60, + "h" | "heure" | "heures" => value * 3600, + "j" | "d" | "jour" | "jours" => value * 86400, + _ => return None, + }; + + Some(Duration::from_secs(secs.max(1))) +} pub async fn handle_loading(ctx: &Context, msg: &Message, args: &[&str]) { - advanced_tools::handle_loading(ctx, msg, args).await; + if args.len() < 2 { + let embed = CreateEmbed::new() + .title("Loading") + .description("Ouvre un modal pour saisir la durée et le message.") + .color(theme_color(ctx).await); + + let components = vec![CreateActionRow::Buttons(vec![ + CreateButton::new(format!("adv:loading:modal:{}", msg.author.id.get())) + .label("Configurer") + .style(serenity::all::ButtonStyle::Primary), + ])]; + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; + return; + } + + let Some(duration) = duration_from_input(args[0]) else { + return; + }; + + let total_secs = duration.as_secs().clamp(1, 120); + let text = args[1..].join(" "); + + let mut sent = match msg + .channel_id + .send_message(&ctx.http, CreateMessage::new().content("[----------] 0%")) + .await + { + Ok(m) => m, + Err(_) => return, + }; + + for i in 0..=10_u64 { + let done = "#".repeat(i as usize); + let todo = "-".repeat((10 - i) as usize); + let percent = i * 10; + + let _ = sent + .edit( + &ctx.http, + EditMessage::new().content(format!("{} [{}{}] {}%", text, done, todo, percent)), + ) + .await; + + if i < 10 { + tokio::time::sleep(Duration::from_secs((total_secs / 10).max(1))).await; + } + } } pub struct LoadingCommand; @@ -13,14 +99,12 @@ pub static COMMAND_DESCRIPTOR: LoadingCommand = LoadingCommand; impl crate::commands::command_contract::CommandSpec for LoadingCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "loading", - command: "loading", + name: "loading", category: "general", params: " ", summary: "Affiche une barre de chargement", description: "Anime une barre de progression avec la duree et le texte fournis.", examples: &["+loading 10s Traitement en cours"], - alias_source_key: "loading", default_aliases: &["loadbar", "bar"], default_permission: 8, } diff --git a/src/commands/general/member.rs b/src/commands/general/member.rs index 8b43a39..7994586 100644 --- a/src/commands/general/member.rs +++ b/src/commands/general/member.rs @@ -77,14 +77,12 @@ pub static COMMAND_DESCRIPTOR: MemberCommand = MemberCommand; impl crate::commands::command_contract::CommandSpec for MemberCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "member", - command: "member", + name: "member", category: "general", params: "<@membre/ID>", summary: "Affiche le profil membre", description: "Affiche les informations dun membre dans le serveur courant.", examples: &["+member", "+mr", "+help member"], - alias_source_key: "member", default_aliases: &["mbr"], default_permission: 0, } diff --git a/src/commands/general/pic.rs b/src/commands/general/pic.rs index 65f084a..eeaa572 100644 --- a/src/commands/general/pic.rs +++ b/src/commands/general/pic.rs @@ -64,14 +64,12 @@ pub static COMMAND_DESCRIPTOR: PicCommand = PicCommand; impl crate::commands::command_contract::CommandSpec for PicCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "pic", - command: "pic", + name: "pic", category: "general", params: "<@membre/ID>", summary: "Affiche la photo de profil", description: "Affiche la photo de profil dun utilisateur cible ou de lauteur.", examples: &["+pic", "+pc", "+help pic"], - alias_source_key: "pic", default_aliases: &["pfp"], default_permission: 0, } diff --git a/src/commands/general/ping.rs b/src/commands/general/ping.rs index d79a19f..f8777ff 100644 --- a/src/commands/general/ping.rs +++ b/src/commands/general/ping.rs @@ -50,14 +50,12 @@ pub static COMMAND_DESCRIPTOR: PingCommand = PingCommand; impl crate::commands::command_contract::CommandSpec for PingCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "ping", - command: "ping", + name: "ping", category: "general", params: "aucun", summary: "Mesure la latence du bot", description: "Affiche le temps de reponse du bot et met a jour un embed avec la latence calculee.", examples: &["+ping", "+pg", "+help ping"], - alias_source_key: "ping", default_aliases: &["pg"], default_permission: 0, } diff --git a/src/commands/general/role.rs b/src/commands/general/role.rs index e7fafc3..4bbbfb0 100644 --- a/src/commands/general/role.rs +++ b/src/commands/general/role.rs @@ -92,14 +92,12 @@ pub static COMMAND_DESCRIPTOR: RoleCommand = RoleCommand; impl crate::commands::command_contract::CommandSpec for RoleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "role", - command: "role", + name: "role", category: "general", params: "<@&rôle/ID>", summary: "Affiche les details dun role", description: "Affiche les informations utiles dun role cible.", examples: &["+role", "+re", "+help role"], - alias_source_key: "role", default_aliases: &["rol"], default_permission: 0, } diff --git a/src/commands/general/rolemembers.rs b/src/commands/general/rolemembers.rs index ce5bad6..d70522a 100644 --- a/src/commands/general/rolemembers.rs +++ b/src/commands/general/rolemembers.rs @@ -86,14 +86,12 @@ pub static COMMAND_DESCRIPTOR: RolemembersCommand = RolemembersCommand; impl crate::commands::command_contract::CommandSpec for RolemembersCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "rolemembers", - command: "rolemembers", + name: "rolemembers", category: "general", params: "<@&rôle/ID>", summary: "Liste les membres dun role", description: "Affiche les membres associes a un role donne.", examples: &["+rolemembers", "+rs", "+help rolemembers"], - alias_source_key: "rolemembers", default_aliases: &["rmb"], default_permission: 0, } diff --git a/src/commands/general/server.rs b/src/commands/general/server.rs index ddc20f6..e3fef88 100644 --- a/src/commands/general/server.rs +++ b/src/commands/general/server.rs @@ -157,14 +157,12 @@ pub static COMMAND_DESCRIPTOR: ServerCommand = ServerCommand; impl crate::commands::command_contract::CommandSpec for ServerCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "server", - command: "server", + name: "server", category: "general", params: "pic | banner | list", summary: "Affiche et gere le serveur", description: "Affiche licone ou la banniere du serveur, ou liste les serveurs du bot selon la sous commande.", examples: &["+server", "+sr", "+help server"], - alias_source_key: "server", default_aliases: &["srv"], default_permission: 0, } diff --git a/src/commands/general/serverinfo.rs b/src/commands/general/serverinfo.rs index 5d6e863..a4f679d 100644 --- a/src/commands/general/serverinfo.rs +++ b/src/commands/general/serverinfo.rs @@ -58,14 +58,12 @@ pub static COMMAND_DESCRIPTOR: ServerinfoCommand = ServerinfoCommand; impl crate::commands::command_contract::CommandSpec for ServerinfoCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "serverinfo", - command: "serverinfo", + name: "serverinfo", category: "general", params: "[ID_serveur]", summary: "Affiche les infos dun serveur", description: "Affiche les informations principales dun serveur comme nom, id et statistiques.", examples: &["+serverinfo", "+so", "+help serverinfo"], - alias_source_key: "serverinfo", default_aliases: &["svi"], default_permission: 0, } diff --git a/src/commands/general/shadowbot.rs b/src/commands/general/shadowbot.rs index f083e64..d6487e3 100644 --- a/src/commands/general/shadowbot.rs +++ b/src/commands/general/shadowbot.rs @@ -22,14 +22,12 @@ pub static COMMAND_DESCRIPTOR: ShadowbotCommand = ShadowbotCommand; impl crate::commands::command_contract::CommandSpec for ShadowbotCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "shadowbot", - command: "shadowbot", + name: "shadowbot", category: "general", params: "aucun", summary: "Affiche les infos du bot", description: "Affiche les informations globales et letat du bot.", examples: &["+shadowbot", "+st", "+help shadowbot"], - alias_source_key: "shadowbot", default_aliases: &["sbt"], default_permission: 0, } diff --git a/src/commands/general/showpics.rs b/src/commands/general/showpics.rs index a1b2b05..f42f112 100644 --- a/src/commands/general/showpics.rs +++ b/src/commands/general/showpics.rs @@ -54,3 +54,21 @@ pub async fn handle_show_pics(ctx: &Context, msg: &Message, args: &[&str]) { send_embed(ctx, msg, embed).await; } } + +pub struct ShowpicsCommand; +pub static COMMAND_DESCRIPTOR: ShowpicsCommand = ShowpicsCommand; + +impl crate::commands::command_contract::CommandSpec for ShowpicsCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "showpics", + category: "general", + params: "[nombre 1-5]", + summary: "Affiche des photos de profil", + description: "Affiche jusqua 5 avatars de membres du serveur.", + examples: &["+show pics", "+help showpics"], + default_aliases: &[], + default_permission: 0, + } + } +} diff --git a/src/commands/general/snipe.rs b/src/commands/general/snipe.rs index 1c8f980..2495530 100644 --- a/src/commands/general/snipe.rs +++ b/src/commands/general/snipe.rs @@ -69,14 +69,12 @@ pub static COMMAND_DESCRIPTOR: SnipeCommand = SnipeCommand; impl crate::commands::command_contract::CommandSpec for SnipeCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "snipe", - command: "snipe", + name: "snipe", category: "general", params: "[index]", summary: "Recupere un message supprime", description: "Affiche le dernier message supprime dans le salon ou un index de messages supprimes.", examples: &["+snipe", "+se", "+help snipe"], - alias_source_key: "snipe", default_aliases: &["snp"], default_permission: 0, } diff --git a/src/commands/general/user.rs b/src/commands/general/user.rs index 802b146..5a354ef 100644 --- a/src/commands/general/user.rs +++ b/src/commands/general/user.rs @@ -64,14 +64,12 @@ pub static COMMAND_DESCRIPTOR: UserCommand = UserCommand; impl crate::commands::command_contract::CommandSpec for UserCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "user", - command: "user", + name: "user", category: "general", params: "<@membre/ID>", summary: "Affiche le profil utilisateur", description: "Affiche les informations principales dun utilisateur cible.", examples: &["+user", "+ur", "+help user"], - alias_source_key: "user", default_aliases: &["usr"], default_permission: 0, } diff --git a/src/commands/general/vocinfo.rs b/src/commands/general/vocinfo.rs index d9972c3..83a2d63 100644 --- a/src/commands/general/vocinfo.rs +++ b/src/commands/general/vocinfo.rs @@ -68,14 +68,12 @@ pub static COMMAND_DESCRIPTOR: VocinfoCommand = VocinfoCommand; impl crate::commands::command_contract::CommandSpec for VocinfoCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "vocinfo", - command: "vocinfo", + name: "vocinfo", category: "general", params: "[ID_salon_vocal]", summary: "Affiche les infos vocales", description: "Affiche les informations dun salon vocal cible ou du salon vocal courant.", examples: &["+vocinfo", "+vo", "+help vocinfo"], - alias_source_key: "vocinfo", default_aliases: &["vci"], default_permission: 0, } diff --git a/src/commands/logs/viewlogs.rs b/src/commands/logs/viewlogs.rs index dcc1f2b..6a6400a 100644 --- a/src/commands/logs/viewlogs.rs +++ b/src/commands/logs/viewlogs.rs @@ -250,14 +250,12 @@ pub static COMMAND_DESCRIPTOR: ViewLogsCommand = ViewLogsCommand; impl crate::commands::command_contract::CommandSpec for ViewLogsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "viewlogs", - command: "viewlogs", + name: "viewlogs", category: "logs", params: "[page]", summary: "Affiche les logs d'audit du serveur", description: "Affiche les derniers logs d'audit du serveur avec pagination. Les logs incluent tous les événements (modération, messages, rôles, salons, etc.)", examples: &["+viewlogs", "+viewlogs 2"], - alias_source_key: "viewlogs", default_aliases: &["vlogs", "audit"], default_permission: 0, } diff --git a/src/commands/logs_command_helpers.rs b/src/commands/logs_command_helpers.rs new file mode 100644 index 0000000..05da676 --- /dev/null +++ b/src/commands/logs_command_helpers.rs @@ -0,0 +1,45 @@ +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::common::parse_channel_id; +use crate::db::DbPoolKey; + +pub async fn pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} + +pub fn parse_target_channel(msg: &Message, args: &[&str], idx: usize) -> Option { + args.get(idx) + .and_then(|raw| parse_channel_id(raw)) + .or(Some(msg.channel_id)) +} + +pub async fn set_log_channel( + ctx: &Context, + guild_id: GuildId, + log_type: &str, + channel_id: Option, + enabled: bool, +) { + let Some(pool) = pool(ctx).await else { + return; + }; + let bot_id = ctx.cache.current_user().id; + + let _ = sqlx::query( + r#" + INSERT INTO bot_log_channels (bot_id, guild_id, log_type, channel_id, enabled) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (bot_id, guild_id, log_type) + DO UPDATE SET channel_id = EXCLUDED.channel_id, enabled = EXCLUDED.enabled, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(log_type) + .bind(channel_id.map(|c| c.get() as i64)) + .bind(enabled) + .execute(&pool) + .await; +} diff --git a/src/commands/logs_service.rs b/src/commands/logs_service.rs index 0176094..0e90e01 100644 --- a/src/commands/logs_service.rs +++ b/src/commands/logs_service.rs @@ -1,63 +1,17 @@ use std::collections::BTreeSet; use chrono::Utc; -use serenity::builder::{CreateChannel, CreateEmbed}; +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::common::{parse_channel_id, send_embed, theme_color}; use crate::db::DbPoolKey; -const LOG_TYPES: &[(&str, &str)] = &[ - ("moderation", "modlog"), - ("message", "messagelog"), - ("voice", "voicelog"), - ("boost", "boostlog"), - ("role", "rolelog"), - ("raid", "raidlog"), - ("channel", "channellog"), -]; - async fn pool(ctx: &Context) -> Option { let data = ctx.data.read().await; data.get::().cloned() } -fn parse_target_channel(msg: &Message, args: &[&str], idx: usize) -> Option { - args.get(idx) - .and_then(|raw| parse_channel_id(raw)) - .or(Some(msg.channel_id)) -} - -async fn set_log_channel( - ctx: &Context, - guild_id: GuildId, - log_type: &str, - channel_id: Option, - enabled: bool, -) { - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let _ = sqlx::query( - r#" - INSERT INTO bot_log_channels (bot_id, guild_id, log_type, channel_id, enabled) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (bot_id, guild_id, log_type) - DO UPDATE SET channel_id = EXCLUDED.channel_id, enabled = EXCLUDED.enabled, updated_at = NOW(); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(log_type) - .bind(channel_id.map(|c| c.get() as i64)) - .bind(enabled) - .execute(&pool) - .await; -} - async fn get_log_channel(ctx: &Context, guild_id: GuildId, log_type: &str) -> Option { let pool = pool(ctx).await?; let bot_id = ctx.cache.current_user().id; @@ -160,348 +114,6 @@ pub async fn emit_log( send_log_embed(ctx, guild_id, log_type, embed).await; } -pub async fn handle_log_toggle( - ctx: &Context, - msg: &Message, - args: &[&str], - log_type: &str, - label: &str, -) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(action) = args.first().map(|s| s.to_lowercase()) else { - let embed = CreateEmbed::new() - .title(label) - .description(format!("Usage: +{} ", label.to_lowercase())) - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - match action.as_str() { - "on" => { - let channel = parse_target_channel(msg, args, 1); - set_log_channel(ctx, guild_id, log_type, channel, true).await; - let embed = CreateEmbed::new() - .title(label) - .description(format!( - "Activé dans {}.", - channel - .map(|c| format!("<#{}>", c.get())) - .unwrap_or_else(|| "ce salon".to_string()) - )) - .color(theme_color(ctx).await); - send_embed(ctx, msg, embed).await; - } - "off" => { - set_log_channel(ctx, guild_id, log_type, None, false).await; - let embed = CreateEmbed::new() - .title(label) - .description("Désactivé.") - .color(theme_color(ctx).await); - send_embed(ctx, msg, embed).await; - } - _ => { - let embed = CreateEmbed::new() - .title(label) - .description(format!("Usage: +{} ", label.to_lowercase())) - .color(0xED4245); - send_embed(ctx, msg, embed).await; - } - } -} - -pub async fn handle_raidlog(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - if args - .first() - .map(|a| a.eq_ignore_ascii_case("off")) - .unwrap_or(false) - { - set_log_channel(ctx, guild_id, "raid", None, false).await; - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("RaidLog") - .description("Désactivé.") - .color(theme_color(ctx).await), - ) - .await; - return; - } - - let channel = parse_target_channel(msg, args, 0); - set_log_channel(ctx, guild_id, "raid", channel, true).await; - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("RaidLog") - .description(format!( - "Activé dans {}.", - channel - .map(|c| format!("<#{}>", c.get())) - .unwrap_or_else(|| "ce salon".to_string()) - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_autoconfiglog(ctx: &Context, msg: &Message) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let mut created = Vec::new(); - for (log_type, cmd) in LOG_TYPES { - let name = format!("{}-logs", cmd.replace("log", "")); - if let Ok(channel) = guild_id - .create_channel(&ctx.http, CreateChannel::new(name).kind(ChannelType::Text)) - .await - { - set_log_channel(ctx, guild_id, log_type, Some(channel.id), true).await; - created.push(format!("{} -> <#{}>", log_type, channel.id.get())); - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("AutoConfigLog") - .description(if created.is_empty() { - "Aucun salon créé.".to_string() - } else { - created.join("\n") - }) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_set_modlogs(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let row = sqlx::query_as::<_, (String,)>( - r#" - SELECT modlog_events - FROM bot_log_settings - WHERE bot_id = $1 AND guild_id = $2 - LIMIT 1; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .fetch_optional(&pool) - .await - .ok() - .flatten(); - - let mut events = row - .map(|(s,)| { - s.split(',') - .map(|v| v.trim().to_lowercase()) - .filter(|v| !v.is_empty()) - .collect::>() - }) - .unwrap_or_else(|| { - [ - "warn", - "mute", - "tempmute", - "unmute", - "cmute", - "tempcmute", - "uncmute", - "kick", - "ban", - "tempban", - "unban", - "lock", - "unlock", - "hide", - "unhide", - "addrole", - "delrole", - "derank", - "clear", - "sanctions", - ] - .into_iter() - .map(|s| s.to_string()) - .collect() - }); - - if args.len() >= 2 { - let event = args[0].to_lowercase(); - let state = args[1].to_lowercase(); - if state == "on" { - events.insert(event); - } else if state == "off" { - events.remove(&event); - } - - let serialized = events.iter().cloned().collect::>().join(","); - let _ = sqlx::query( - r#" - INSERT INTO bot_log_settings (bot_id, guild_id, modlog_events) - VALUES ($1, $2, $3) - ON CONFLICT (bot_id, guild_id) - DO UPDATE SET modlog_events = EXCLUDED.modlog_events, updated_at = NOW(); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(serialized) - .execute(&pool) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Set ModLogs") - .description(format!( - "Événements actifs:\n{}\n\nUsage: +set modlogs ", - events.iter().cloned().collect::>().join(", ") - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_join_leave_settings(ctx: &Context, msg: &Message, args: &[&str], kind: &str) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - if args.is_empty() || !args[0].eq_ignore_ascii_case("settings") { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(format!("{} settings", kind)) - .description(format!( - "Usage: +{} settings [on/off] [salon] [message...]", - kind - )) - .color(0xED4245), - ) - .await; - return; - } - - if args.len() == 1 { - let row = sqlx::query_as::<_, (bool, Option, Option)>( - r#" - SELECT enabled, channel_id, custom_message - FROM bot_join_leave_settings - WHERE bot_id = $1 AND guild_id = $2 AND kind = $3 - LIMIT 1; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(kind) - .fetch_optional(&pool) - .await - .ok() - .flatten(); - - let desc = if let Some((enabled, channel_id, custom_message)) = row { - format!( - "État: {}\nSalon: {}\nMessage: {}", - if enabled { "on" } else { "off" }, - channel_id - .map(|id| format!("<#{}>", id)) - .unwrap_or_else(|| "non défini".to_string()), - custom_message.unwrap_or_else(|| "(défaut)".to_string()) - ) - } else { - "Aucun réglage configuré.".to_string() - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(format!("{} settings", kind)) - .description(desc) - .color(theme_color(ctx).await), - ) - .await; - return; - } - - let action = args[1].to_lowercase(); - let enabled = action == "on"; - let channel = if enabled { - parse_target_channel(msg, args, 2) - } else { - None - }; - let message_start = if enabled { 3 } else { 2 }; - let custom_message = if args.len() > message_start { - Some(args[message_start..].join(" ")) - } else { - None - }; - - let _ = sqlx::query( - r#" - INSERT INTO bot_join_leave_settings (bot_id, guild_id, kind, enabled, channel_id, custom_message) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (bot_id, guild_id, kind) - DO UPDATE SET enabled = EXCLUDED.enabled, channel_id = EXCLUDED.channel_id, - custom_message = EXCLUDED.custom_message, updated_at = NOW(); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(kind) - .bind(enabled) - .bind(channel.map(|c| c.get() as i64)) - .bind(custom_message) - .execute(&pool) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(format!("{} settings", kind)) - .description(format!( - "{} {}", - if enabled { "Activé" } else { "Désactivé" }, - channel - .map(|c| format!("dans <#{}>", c.get())) - .unwrap_or_default() - )) - .color(theme_color(ctx).await), - ) - .await; -} - pub async fn on_member_join(ctx: &Context, guild_id: GuildId, user: &User) { if let Some(channel_id) = get_log_channel(ctx, guild_id, "raid").await { let _ = channel_id @@ -569,94 +181,6 @@ async fn run_join_leave_action(ctx: &Context, guild_id: GuildId, kind: &str, use let _ = channel_id.say(&ctx.http, content).await; } -pub async fn handle_set_boostembed(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.len() < 2 { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Set BoostEmbed") - .description("Usage: +set boostembed ") - .color(0xED4245), - ) - .await; - return; - } - - let field = args[0].to_lowercase(); - let value = args[1..].join(" "); - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let _ = sqlx::query( - r#" - INSERT INTO bot_boost_embed (bot_id, guild_id, enabled, title, description, color) - VALUES ($1, $2, TRUE, NULL, NULL, NULL) - ON CONFLICT (bot_id, guild_id) - DO NOTHING; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .execute(&pool) - .await; - - match field.as_str() { - "title" => { - let _ = sqlx::query( - "UPDATE bot_boost_embed SET title = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(value) - .execute(&pool) - .await; - } - "description" => { - let _ = sqlx::query( - "UPDATE bot_boost_embed SET description = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(value) - .execute(&pool) - .await; - } - "color" => { - let normalized = value - .trim() - .trim_start_matches('#') - .trim_start_matches("0x"); - if let Ok(color) = u32::from_str_radix(normalized, 16) { - let _ = sqlx::query( - "UPDATE bot_boost_embed SET color = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(color as i32) - .execute(&pool) - .await; - } - } - _ => {} - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Set BoostEmbed") - .description("Configuration mise à jour.") - .color(theme_color(ctx).await), - ) - .await; -} - pub async fn send_boost_embed(ctx: &Context, guild_id: GuildId, user: &User) { let Some(pool) = pool(ctx).await else { return; @@ -705,104 +229,6 @@ pub async fn send_boost_embed(ctx: &Context, guild_id: GuildId, user: &User) { send_log_embed(ctx, guild_id, "boost", embed).await; } -pub async fn handle_nolog(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("NoLog") - .description("Usage: +nolog [salon] [message|voice|all]") - .color(0xED4245), - ) - .await; - return; - } - - let action = args[0].to_lowercase(); - let channel = parse_target_channel(msg, args, 1).unwrap_or(msg.channel_id); - let scope = args - .get(2) - .map(|s| s.to_lowercase()) - .unwrap_or_else(|| "all".to_string()); - - let set_message = scope == "all" || scope == "message"; - let set_voice = scope == "all" || scope == "voice"; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - if action == "add" { - let _ = sqlx::query( - r#" - INSERT INTO bot_nolog_channels (bot_id, guild_id, channel_id, disable_message, disable_voice) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (bot_id, guild_id, channel_id) - DO UPDATE SET disable_message = bot_nolog_channels.disable_message OR EXCLUDED.disable_message, - disable_voice = bot_nolog_channels.disable_voice OR EXCLUDED.disable_voice, - updated_at = NOW(); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(channel.get() as i64) - .bind(set_message) - .bind(set_voice) - .execute(&pool) - .await; - } else if action == "del" { - let _ = sqlx::query( - r#" - UPDATE bot_nolog_channels - SET disable_message = CASE WHEN $4 THEN FALSE ELSE disable_message END, - disable_voice = CASE WHEN $5 THEN FALSE ELSE disable_voice END, - updated_at = NOW() - WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(channel.get() as i64) - .bind(set_message) - .bind(set_voice) - .execute(&pool) - .await; - - let _ = sqlx::query( - r#" - DELETE FROM bot_nolog_channels - WHERE bot_id = $1 AND guild_id = $2 AND channel_id = $3 - AND disable_message = FALSE AND disable_voice = FALSE; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(channel.get() as i64) - .execute(&pool) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("NoLog") - .description(format!( - "{} appliqué sur <#{}> ({})", - action, - channel.get(), - scope - )) - .color(theme_color(ctx).await), - ) - .await; -} - pub async fn on_message_deleted( ctx: &Context, guild_id: Option, diff --git a/src/commands/mod.rs b/src/commands/mod.rs index cf72f12..a0aae5b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -133,6 +133,7 @@ pub mod loading; pub mod lock; #[path = "admin/lockall.rs"] pub mod lockall; +pub mod logs_command_helpers; pub mod logs_service; #[path = "permissions/mainprefix.rs"] pub mod mainprefix; @@ -142,6 +143,8 @@ pub mod massiverole; pub mod member; #[path = "admin/messagelog.rs"] pub mod messagelog; +pub mod moderation_channel_helpers; +pub mod moderation_sanction_helpers; pub mod moderation_tools; #[path = "admin/modlog.rs"] pub mod modlog; @@ -161,6 +164,7 @@ pub mod online; pub mod owner; #[path = "permissions/perms.rs"] pub mod perms; +pub mod perms_helpers; pub mod perms_service; #[path = "general/pic.rs"] pub mod pic; @@ -314,6 +318,17 @@ pub fn all_command_metadata() -> Vec { embed::COMMAND_DESCRIPTOR.metadata(), clear_messages::COMMAND_DESCRIPTOR.metadata(), backup::COMMAND_DESCRIPTOR.metadata(), + ticket::COMMAND_DESCRIPTOR.metadata(), + claim::COMMAND_DESCRIPTOR.metadata(), + rename::COMMAND_DESCRIPTOR.metadata(), + ticket_member::COMMAND_DESCRIPTOR.metadata(), + close::COMMAND_DESCRIPTOR.metadata(), + tickets::COMMAND_DESCRIPTOR.metadata(), + showpics::COMMAND_DESCRIPTOR.metadata(), + suggestion::COMMAND_DESCRIPTOR.metadata(), + autopublish::COMMAND_DESCRIPTOR.metadata(), + tempvoc::COMMAND_DESCRIPTOR.metadata(), + tempvoc_cmd::COMMAND_DESCRIPTOR.metadata(), autobackup::COMMAND_DESCRIPTOR.metadata(), loading::COMMAND_DESCRIPTOR.metadata(), create::COMMAND_DESCRIPTOR.metadata(), @@ -404,7 +419,7 @@ pub fn all_command_metadata() -> Vec { pub fn command_metadata_by_key(key: &str) -> Option { all_command_metadata() .into_iter() - .find(|meta| meta.key == key) + .find(|meta| meta.name == key) } pub fn resolve_default_alias(alias: &str) -> Option<&'static str> { @@ -415,7 +430,7 @@ pub fn resolve_default_alias(alias: &str) -> Option<&'static str> { .iter() .any(|candidate| candidate.eq_ignore_ascii_case(&normalized)) { - Some(meta.key) + Some(meta.name) } else { None } diff --git a/src/commands/moderation_channel_helpers.rs b/src/commands/moderation_channel_helpers.rs new file mode 100644 index 0000000..71253bc --- /dev/null +++ b/src/commands/moderation_channel_helpers.rs @@ -0,0 +1,67 @@ +use serenity::model::prelude::*; +use serenity::prelude::*; + +pub async fn edit_channel_visibility( + ctx: &Context, + guild_id: GuildId, + channel_id: ChannelId, + lock: Option, + hide: Option, +) -> bool { + let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { + return false; + }; + + let everyone_role = guild + .roles + .values() + .find(|r| r.name == "@everyone") + .map(|r| r.id); + let Some(everyone_role) = everyone_role else { + return false; + }; + + let Ok(channels) = guild_id.channels(&ctx.http).await else { + return false; + }; + let Some(channel) = channels.get(&channel_id) else { + return false; + }; + + let mut allow = Permissions::empty(); + let mut deny = Permissions::empty(); + + if let Some(locked) = lock { + if channel.kind == ChannelType::Text || channel.kind == ChannelType::News { + if locked { + deny |= Permissions::SEND_MESSAGES; + } else { + allow |= Permissions::SEND_MESSAGES; + } + } else if locked { + deny |= Permissions::CONNECT | Permissions::SPEAK; + } else { + allow |= Permissions::CONNECT | Permissions::SPEAK; + } + } + + if let Some(hidden) = hide { + if hidden { + deny |= Permissions::VIEW_CHANNEL; + } else { + allow |= Permissions::VIEW_CHANNEL; + } + } + + channel_id + .create_permission( + &ctx.http, + PermissionOverwrite { + allow, + deny, + kind: PermissionOverwriteType::Role(everyone_role), + }, + ) + .await + .is_ok() +} diff --git a/src/commands/moderation_sanction_helpers.rs b/src/commands/moderation_sanction_helpers.rs new file mode 100644 index 0000000..72140c6 --- /dev/null +++ b/src/commands/moderation_sanction_helpers.rs @@ -0,0 +1,150 @@ +use std::time::Duration; + +use chrono::Utc; +use serenity::builder::EditMember; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::admin_common::parse_user_id; +use crate::db::DbPoolKey; + +pub fn duration_from_input(input: &str) -> Option { + let raw = input.trim().to_lowercase(); + if raw.is_empty() { + return None; + } + + let mut number = String::new(); + let mut suffix = String::new(); + for ch in raw.chars() { + if ch.is_ascii_digit() { + if !suffix.is_empty() { + return None; + } + number.push(ch); + } else if !ch.is_whitespace() { + suffix.push(ch); + } + } + + let value = number.parse::().ok()?; + let secs = match suffix.as_str() { + "s" | "sec" | "secs" | "seconde" | "secondes" => value, + "m" | "min" | "mins" | "minute" | "minutes" => value * 60, + "h" | "heure" | "heures" => value * 3600, + "j" | "d" | "jour" | "jours" => value * 86400, + _ => return None, + }; + + Some(Duration::from_secs(secs.max(1))) +} + +pub async fn pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} + +pub async fn add_sanction( + ctx: &Context, + guild_id: GuildId, + user_id: UserId, + moderator_id: UserId, + kind: &str, + reason: &str, + channel_id: Option, + expires_at: Option>, +) { + let Some(pool) = pool(ctx).await else { + return; + }; + + let bot_id = ctx.cache.current_user().id; + let _ = sqlx::query( + r#" + INSERT INTO bot_sanctions + (bot_id, guild_id, user_id, moderator_id, kind, reason, channel_id, expires_at, active) + VALUES + ($1, $2, $3, $4, $5, $6, $7, $8, TRUE); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(user_id.get() as i64) + .bind(moderator_id.get() as i64) + .bind(kind) + .bind(reason) + .bind(channel_id.map(|c| c.get() as i64)) + .bind(expires_at) + .execute(&pool) + .await; +} + +pub async fn parse_targets(raw: &str) -> Vec { + let mut out = Vec::new(); + for token in raw.split(',') { + if let Some(uid) = parse_user_id(token.trim()) { + out.push(uid); + } + } + out +} + +pub async fn handle_timeout( + ctx: &Context, + guild_id: GuildId, + users: &[UserId], + expires: Option>, +) -> usize { + let mut done = 0usize; + for user_id in users { + if let Ok(mut member) = guild_id.member(&ctx.http, *user_id).await { + let mut builder = EditMember::new(); + if let Some(ts) = expires { + if let Ok(discord_ts) = Timestamp::from_unix_timestamp(ts.timestamp()) { + builder = builder.disable_communication_until_datetime(discord_ts); + } + } else { + builder = builder.enable_communication(); + } + + if member.edit(&ctx.http, builder).await.is_ok() { + done += 1; + } + } + } + done +} + +pub async fn channel_mute_users( + ctx: &Context, + channel_id: ChannelId, + users: &[UserId], + mute: bool, +) -> usize { + let mut done = 0usize; + for user_id in users { + let result = if mute { + channel_id + .create_permission( + &ctx.http, + PermissionOverwrite { + allow: Permissions::empty(), + deny: Permissions::SEND_MESSAGES + | Permissions::ADD_REACTIONS + | Permissions::SPEAK, + kind: PermissionOverwriteType::Member(*user_id), + }, + ) + .await + } else { + channel_id + .delete_permission(&ctx.http, PermissionOverwriteType::Member(*user_id)) + .await + }; + + if result.is_ok() { + done += 1; + } + } + done +} diff --git a/src/commands/moderation_tools.rs b/src/commands/moderation_tools.rs index e262ec5..114766c 100644 --- a/src/commands/moderation_tools.rs +++ b/src/commands/moderation_tools.rs @@ -2,97 +2,19 @@ use std::sync::{Mutex, OnceLock}; use std::time::{Duration, Instant}; use chrono::Utc; -use serenity::builder::CreateEmbed; +use serenity::builder::EditMember; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::admin_common::parse_user_id; -use crate::commands::common::{parse_channel_id, parse_role, send_embed, theme_color}; use crate::db::DbPoolKey; static MODERATION_TICK: OnceLock> = OnceLock::new(); -fn duration_from_input(input: &str) -> Option { - let raw = input.trim().to_lowercase(); - if raw.is_empty() { - return None; - } - - let mut number = String::new(); - let mut suffix = String::new(); - for ch in raw.chars() { - if ch.is_ascii_digit() { - if !suffix.is_empty() { - return None; - } - number.push(ch); - } else if !ch.is_whitespace() { - suffix.push(ch); - } - } - - let value = number.parse::().ok()?; - let secs = match suffix.as_str() { - "s" | "sec" | "secs" | "seconde" | "secondes" => value, - "m" | "min" | "mins" | "minute" | "minutes" => value * 60, - "h" | "heure" | "heures" => value * 3600, - "j" | "d" | "jour" | "jours" => value * 86400, - _ => return None, - }; - - Some(Duration::from_secs(secs.max(1))) -} - async fn pool(ctx: &Context) -> Option { let data = ctx.data.read().await; data.get::().cloned() } -async fn add_sanction( - ctx: &Context, - guild_id: GuildId, - user_id: UserId, - moderator_id: UserId, - kind: &str, - reason: &str, - channel_id: Option, - expires_at: Option>, -) { - let Some(pool) = pool(ctx).await else { - return; - }; - - let bot_id = ctx.cache.current_user().id; - let _ = sqlx::query( - r#" - INSERT INTO bot_sanctions - (bot_id, guild_id, user_id, moderator_id, kind, reason, channel_id, expires_at, active) - VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, TRUE); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(user_id.get() as i64) - .bind(moderator_id.get() as i64) - .bind(kind) - .bind(reason) - .bind(channel_id.map(|c| c.get() as i64)) - .bind(expires_at) - .execute(&pool) - .await; -} - -async fn parse_targets(raw: &str) -> Vec { - let mut out = Vec::new(); - for token in raw.split(',') { - if let Some(uid) = parse_user_id(token.trim()) { - out.push(uid); - } - } - out -} - async fn handle_timeout( ctx: &Context, guild_id: GuildId, @@ -102,7 +24,7 @@ async fn handle_timeout( let mut done = 0usize; for user_id in users { if let Ok(mut member) = guild_id.member(&ctx.http, *user_id).await { - let mut builder = serenity::builder::EditMember::new(); + let mut builder = EditMember::new(); if let Some(ts) = expires { if let Ok(discord_ts) = Timestamp::from_unix_timestamp(ts.timestamp()) { builder = builder.disable_communication_until_datetime(discord_ts); @@ -153,1100 +75,6 @@ async fn channel_mute_users( done } -pub async fn handle_sanctions(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - let Some(target_raw) = args.first() else { - let _ = send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Sanctions") - .description("Usage: +sanctions ") - .color(0xED4245), - ) - .await; - return; - }; - let Some(target) = parse_user_id(target_raw) else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let rows = sqlx::query_as::< - _, - ( - i64, - String, - String, - chrono::DateTime, - Option>, - bool, - ), - >( - r#" - SELECT id, kind, reason, created_at, expires_at, active - FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 - ORDER BY created_at DESC - LIMIT 30; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(target.get() as i64) - .fetch_all(&pool) - .await - .unwrap_or_default(); - - let desc = if rows.is_empty() { - "Aucune sanction.".to_string() - } else { - rows.into_iter() - .map(|(id, kind, reason, created_at, expires_at, active)| { - let until = expires_at - .map(|d| format!(" · jusqu'à ", d.timestamp())) - .unwrap_or_default(); - format!( - "`#{}` `{}` {} · {} · {}", - id, - kind, - if active { "(active)" } else { "(inactive)" }, - created_at.timestamp(), - until, - reason - ) - }) - .collect::>() - .join("\n") - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(format!("Sanctions de <@{}>", target.get())) - .description(desc) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_del_sanction(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.len() < 3 { - return; - } - - let Some(target) = parse_user_id(args[1]) else { - return; - }; - let Ok(index) = args[2].parse::() else { - return; - }; - if index == 0 { - return; - } - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let rows = sqlx::query_as::<_, (i64,)>( - r#" - SELECT id - FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 - ORDER BY created_at DESC; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(target.get() as i64) - .fetch_all(&pool) - .await - .unwrap_or_default(); - - let Some((sanction_id,)) = rows.get(index - 1).copied() else { - return; - }; - - let _ = sqlx::query( - r#" - DELETE FROM bot_sanctions - WHERE id = $1 AND bot_id = $2 AND guild_id = $3; - "#, - ) - .bind(sanction_id) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .execute(&pool) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Sanctions") - .description(format!( - "Sanction #{} supprimée pour <@{}>.", - sanction_id, - target.get() - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_clear_sanctions(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.len() < 2 { - return; - } - - let Some(target) = parse_user_id(args[1]) else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let removed = sqlx::query( - r#" - DELETE FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(target.get() as i64) - .execute(&pool) - .await - .ok() - .map(|r| r.rows_affected()) - .unwrap_or(0); - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Sanctions") - .description(format!( - "{} sanction(s) supprimée(s) pour <@{}>.", - removed, - target.get() - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_clear_all_sanctions(ctx: &Context, msg: &Message) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let removed = sqlx::query( - r#" - DELETE FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .execute(&pool) - .await - .ok() - .map(|r| r.rows_affected()) - .unwrap_or(0); - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Sanctions") - .description(format!( - "{} sanction(s) supprimée(s) sur le serveur.", - removed - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_clear_messages(ctx: &Context, msg: &Message, args: &[&str]) { - let Ok(mut amount) = args.first().unwrap_or(&"0").parse::() else { - return; - }; - if amount == 0 { - return; - } - amount = amount.clamp(1, 100); - - let filter_user = args.get(1).and_then(|raw| parse_user_id(raw)); - - let mut deleted = 0usize; - if let Ok(messages) = msg - .channel_id - .messages( - &ctx.http, - serenity::builder::GetMessages::new().limit(amount as u8 + 1), - ) - .await - { - for m in messages { - if m.id == msg.id { - continue; - } - if let Some(user_id) = filter_user { - if m.author.id != user_id { - continue; - } - } - if msg.channel_id.delete_message(&ctx.http, m.id).await.is_ok() { - deleted += 1; - } - } - } - - let _ = send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Clear") - .description(format!("{} message(s) supprimé(s).", deleted)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_warn(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - let reason = if args.len() > 1 { - args[1..].join(" ") - } else { - "Aucune raison".to_string() - }; - - for uid in &targets { - add_sanction( - ctx, - guild_id, - *uid, - msg.author.id, - "warn", - &reason, - None, - None, - ) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Warn") - .description(format!("{} membre(s) warn.", targets.len())) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_mute(ctx: &Context, msg: &Message, args: &[&str], temporary: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let (expires_at, reason_start_idx) = if temporary { - let Some(duration_raw) = args.get(1) else { - return; - }; - let Some(duration) = duration_from_input(duration_raw) else { - return; - }; - ( - Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)), - 2, - ) - } else { - // "indéfini" = 28 jours max via timeout Discord. - ( - Some(Utc::now() + chrono::Duration::seconds(28 * 24 * 3600)), - 1, - ) - }; - - let reason = if args.len() > reason_start_idx { - args[reason_start_idx..].join(" ") - } else { - "Aucune raison".to_string() - }; - - let affected = handle_timeout(ctx, guild_id, &targets, expires_at).await; - - for uid in &targets { - add_sanction( - ctx, - guild_id, - *uid, - msg.author.id, - if temporary { "tempmute" } else { "mute" }, - &reason, - None, - expires_at, - ) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if temporary { "TempMute" } else { "Mute" }) - .description(format!("{} membre(s) mute.", affected)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_unmute(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let affected = handle_timeout(ctx, guild_id, &targets, None).await; - - if let Some(pool) = pool(ctx).await { - let bot_id = ctx.cache.current_user().id; - for uid in &targets { - let _ = sqlx::query( - r#" - UPDATE bot_sanctions - SET active = FALSE - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND active = TRUE AND kind IN ('mute','tempmute'); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(uid.get() as i64) - .execute(&pool) - .await; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnMute") - .description(format!("{} membre(s) unmute.", affected)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_cmute(ctx: &Context, msg: &Message, args: &[&str], temporary: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let (expires_at, reason_start_idx) = if temporary { - let Some(duration_raw) = args.get(1) else { - return; - }; - let Some(duration) = duration_from_input(duration_raw) else { - return; - }; - ( - Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)), - 2, - ) - } else { - (None, 1) - }; - - let reason = if args.len() > reason_start_idx { - args[reason_start_idx..].join(" ") - } else { - "Aucune raison".to_string() - }; - - let affected = channel_mute_users(ctx, msg.channel_id, &targets, true).await; - - for uid in &targets { - add_sanction( - ctx, - guild_id, - *uid, - msg.author.id, - if temporary { "tempcmute" } else { "cmute" }, - &reason, - Some(msg.channel_id), - expires_at, - ) - .await; - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if temporary { "TempCMute" } else { "CMute" }) - .description(format!("{} membre(s) cmute.", affected)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_uncmute(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let affected = channel_mute_users(ctx, msg.channel_id, &targets, false).await; - - if let Some(pool) = pool(ctx).await { - let bot_id = ctx.cache.current_user().id; - for uid in &targets { - let _ = sqlx::query( - r#" - UPDATE bot_sanctions - SET active = FALSE - WHERE bot_id = $1 AND guild_id = $2 AND user_id = $3 AND active = TRUE AND kind IN ('cmute','tempcmute') AND channel_id = $4; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(uid.get() as i64) - .bind(msg.channel_id.get() as i64) - .execute(&pool) - .await; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnCMute") - .description(format!("{} membre(s) uncmute.", affected)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_mutelist(ctx: &Context, msg: &Message) { - let Some(guild_id) = msg.guild_id else { - return; - }; - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let rows = sqlx::query_as::<_, (i64, String, Option, Option>)>( - r#" - SELECT user_id, kind, channel_id, expires_at - FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute') - ORDER BY created_at DESC - LIMIT 60; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .fetch_all(&pool) - .await - .unwrap_or_default(); - - let desc = if rows.is_empty() { - "Aucun mute en cours.".to_string() - } else { - rows.into_iter() - .map(|(uid, kind, channel_id, exp)| { - let channel = channel_id - .map(|c| format!(" dans <#{}>", c)) - .unwrap_or_default(); - let until = exp - .map(|d| format!(" jusqu'à ", d.timestamp())) - .unwrap_or_default(); - format!("- <@{}> `{}`{}{}", uid, kind, channel, until) - }) - .collect::>() - .join("\n") - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("MuteList") - .description(desc) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_unmuteall(ctx: &Context, msg: &Message) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let Some(pool) = pool(ctx).await else { - return; - }; - let bot_id = ctx.cache.current_user().id; - - let rows = sqlx::query_as::<_, (i64, String, Option)>( - r#" - SELECT user_id, kind, channel_id - FROM bot_sanctions - WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute'); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .fetch_all(&pool) - .await - .unwrap_or_default(); - - let mut changed = 0usize; - for (uid, kind, channel_id) in rows { - let user_id = UserId::new(uid as u64); - if kind == "mute" || kind == "tempmute" { - changed += handle_timeout(ctx, guild_id, &[user_id], None).await; - } else if let Some(cid) = channel_id { - changed += channel_mute_users(ctx, ChannelId::new(cid as u64), &[user_id], false).await; - } - } - - let _ = sqlx::query( - r#" - UPDATE bot_sanctions - SET active = FALSE - WHERE bot_id = $1 AND guild_id = $2 AND active = TRUE AND kind IN ('mute','tempmute','cmute','tempcmute'); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .execute(&pool) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnMuteAll") - .description(format!( - "{} opération(s) de unmute/cmute annulé(es).", - changed - )) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_kick(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let reason = if args.len() > 1 { - args[1..].join(" ") - } else { - "Aucune raison".to_string() - }; - - let mut done = 0usize; - for uid in &targets { - if guild_id - .kick_with_reason(&ctx.http, *uid, &reason) - .await - .is_ok() - { - done += 1; - add_sanction( - ctx, - guild_id, - *uid, - msg.author.id, - "kick", - &reason, - None, - None, - ) - .await; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Kick") - .description(format!("{} membre(s) expulsé(s).", done)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_ban(ctx: &Context, msg: &Message, args: &[&str], temporary: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let (expires_at, reason_start_idx) = if temporary { - let Some(duration_raw) = args.get(1) else { - return; - }; - let Some(duration) = duration_from_input(duration_raw) else { - return; - }; - ( - Some(Utc::now() + chrono::Duration::seconds(duration.as_secs() as i64)), - 2, - ) - } else { - (None, 1) - }; - - let reason = if args.len() > reason_start_idx { - args[reason_start_idx..].join(" ") - } else { - "Aucune raison".to_string() - }; - - let mut done = 0usize; - for uid in &targets { - if guild_id - .ban_with_reason(&ctx.http, *uid, 0, &reason) - .await - .is_ok() - { - done += 1; - add_sanction( - ctx, - guild_id, - *uid, - msg.author.id, - if temporary { "tempban" } else { "ban" }, - &reason, - None, - expires_at, - ) - .await; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if temporary { "TempBan" } else { "Ban" }) - .description(format!("{} membre(s) banni(s).", done)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_unban(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let mut done = 0usize; - for uid in &targets { - if guild_id.unban(&ctx.http, *uid).await.is_ok() { - done += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("UnBan") - .description(format!("{} membre(s) unban.", done)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_banlist(ctx: &Context, msg: &Message) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let bans = guild_id - .bans(&ctx.http, None, None) - .await - .unwrap_or_default(); - let desc = if bans.is_empty() { - "Aucun ban en cours.".to_string() - } else { - bans.into_iter() - .map(|ban| format!("- <@{}> ({})", ban.user.id.get(), ban.user.tag())) - .collect::>() - .join("\n") - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("BanList") - .description(desc) - .color(theme_color(ctx).await), - ) - .await; -} - -async fn edit_channel_visibility( - ctx: &Context, - guild_id: GuildId, - channel_id: ChannelId, - lock: Option, - hide: Option, -) -> bool { - let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { - return false; - }; - - let everyone_role = guild - .roles - .values() - .find(|r| r.name == "@everyone") - .map(|r| r.id); - let Some(everyone_role) = everyone_role else { - return false; - }; - - let Ok(channels) = guild_id.channels(&ctx.http).await else { - return false; - }; - let Some(channel) = channels.get(&channel_id) else { - return false; - }; - - let mut allow = Permissions::empty(); - let mut deny = Permissions::empty(); - - if let Some(locked) = lock { - if channel.kind == ChannelType::Text || channel.kind == ChannelType::News { - if locked { - deny |= Permissions::SEND_MESSAGES; - } else { - allow |= Permissions::SEND_MESSAGES; - } - } else { - if locked { - deny |= Permissions::CONNECT | Permissions::SPEAK; - } else { - allow |= Permissions::CONNECT | Permissions::SPEAK; - } - } - } - - if let Some(hidden) = hide { - if hidden { - deny |= Permissions::VIEW_CHANNEL; - } else { - allow |= Permissions::VIEW_CHANNEL; - } - } - - channel_id - .create_permission( - &ctx.http, - PermissionOverwrite { - allow, - deny, - kind: PermissionOverwriteType::Role(everyone_role), - }, - ) - .await - .is_ok() -} - -pub async fn handle_lock_unlock(ctx: &Context, msg: &Message, args: &[&str], lock: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let target = args - .first() - .and_then(|raw| parse_channel_id(raw)) - .unwrap_or(msg.channel_id); - - let ok = edit_channel_visibility(ctx, guild_id, target, Some(lock), None).await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if lock { "Lock" } else { "Unlock" }) - .description(if ok { - format!("Salon <#{}> mis à jour.", target.get()) - } else { - "Échec de mise à jour du salon.".to_string() - }) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_hide_unhide(ctx: &Context, msg: &Message, args: &[&str], hide: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - - let target = args - .first() - .and_then(|raw| parse_channel_id(raw)) - .unwrap_or(msg.channel_id); - - let ok = edit_channel_visibility(ctx, guild_id, target, None, Some(hide)).await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if hide { "Hide" } else { "UnHide" }) - .description(if ok { - format!("Salon <#{}> mis à jour.", target.get()) - } else { - "Échec de mise à jour du salon.".to_string() - }) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_lockall_unlockall(ctx: &Context, msg: &Message, lock: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - let Ok(channels) = guild_id.channels(&ctx.http).await else { - return; - }; - - let mut changed = 0usize; - for channel_id in channels.keys() { - if edit_channel_visibility(ctx, guild_id, *channel_id, Some(lock), None).await { - changed += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if lock { "LockAll" } else { "UnlockAll" }) - .description(format!("{} salon(s) mis à jour.", changed)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_hideall_unhideall(ctx: &Context, msg: &Message, hide: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - let Ok(channels) = guild_id.channels(&ctx.http).await else { - return; - }; - - let mut changed = 0usize; - for channel_id in channels.keys() { - if edit_channel_visibility(ctx, guild_id, *channel_id, None, Some(hide)).await { - changed += 1; - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if hide { "HideAll" } else { "UnHideAll" }) - .description(format!("{} salon(s) mis à jour.", changed)) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_add_del_role(ctx: &Context, msg: &Message, args: &[&str], add: bool) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.len() < 2 { - return; - } - - let Some(target) = parse_user_id(args[0]) else { - return; - }; - let Ok(guild) = guild_id.to_partial_guild(&ctx.http).await else { - return; - }; - let Some(role) = parse_role(&guild, args[1]) else { - return; - }; - - let done = if let Ok(member) = guild_id.member(&ctx.http, target).await { - let r = if add { - member.add_role(&ctx.http, role.id).await - } else { - member.remove_role(&ctx.http, role.id).await - }; - r.is_ok() - } else { - false - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title(if add { "AddRole" } else { "DelRole" }) - .description(if done { - format!( - "Rôle <@&{}> {} à <@{}>.", - role.id.get(), - if add { "ajouté" } else { "retiré" }, - target.get() - ) - } else { - "Échec de modification du rôle.".to_string() - }) - .color(theme_color(ctx).await), - ) - .await; -} - -pub async fn handle_derank(ctx: &Context, msg: &Message, args: &[&str]) { - let Some(guild_id) = msg.guild_id else { - return; - }; - if args.is_empty() { - return; - } - - let targets = parse_targets(args[0]).await; - if targets.is_empty() { - return; - } - - let mut done = 0usize; - for uid in &targets { - if let Ok(member) = guild_id.member(&ctx.http, *uid).await { - let roles = member.roles.clone(); - let mut ok = true; - for role_id in roles { - if member.remove_role(&ctx.http, role_id).await.is_err() { - ok = false; - } - } - if ok { - done += 1; - } - } - } - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("Derank") - .description(format!("{} membre(s) dérank.", done)) - .color(theme_color(ctx).await), - ) - .await; -} - pub async fn maybe_run_maintenance(ctx: &Context, guild_id: Option) { let Some(guild_id) = guild_id else { return; diff --git a/src/commands/permissions/alias.rs b/src/commands/permissions/alias.rs index d3bc8f6..9d01a0f 100644 --- a/src/commands/permissions/alias.rs +++ b/src/commands/permissions/alias.rs @@ -123,14 +123,12 @@ pub static COMMAND_DESCRIPTOR: AliasCommand = AliasCommand; impl crate::commands::command_contract::CommandSpec for AliasCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "alias", - command: "alias", + name: "alias", category: "permissions", params: " | remove | list", summary: "Gere les aliases personnalises", description: "Liste, ajoute ou supprime des aliases de commandes stockes en base.", examples: &["+alias", "+as", "+help alias"], - alias_source_key: "alias", default_aliases: &["als"], default_permission: 9, } diff --git a/src/commands/permissions/allperms.rs b/src/commands/permissions/allperms.rs index 4911c5d..2914580 100644 --- a/src/commands/permissions/allperms.rs +++ b/src/commands/permissions/allperms.rs @@ -1,10 +1,121 @@ +use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage}; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::perms_helpers::ensure_owner; +use crate::commands::{common::theme_color, common::truncate_text}; +use crate::permissions::{all_command_keys, command_required_permission, default_permission}; + +const ALLPERMS_PAGE_SIZE: usize = 12; +const ALLPERMS_CUSTOM_ID_PREFIX: &str = "allperms"; + +async fn collect_allperms_lines(ctx: &Context) -> Vec { + let mut commands = all_command_keys(); + if !commands.iter().any(|c| c == "allperms") { + commands.push("allperms".to_string()); + } + commands.sort(); + + let mut lines = Vec::with_capacity(commands.len()); + for cmd in commands { + let required = command_required_permission(ctx, &cmd).await; + let default = default_permission(&cmd); + + if required == default { + lines.push(format!("`{}` -> `{}`", cmd, required)); + } else { + lines.push(format!( + "`{}` -> `{}` (defaut `{}`)", + cmd, required, default + )); + } + } + + lines +} + +fn total_pages_for(total_items: usize) -> usize { + ((total_items + ALLPERMS_PAGE_SIZE.saturating_sub(1)) / ALLPERMS_PAGE_SIZE).max(1) +} + +fn build_allperms_embed(lines: &[String], page: usize, color: u32) -> CreateEmbed { + let total_pages = total_pages_for(lines.len()); + let safe_page = page.min(total_pages.saturating_sub(1)); + let start = safe_page * ALLPERMS_PAGE_SIZE; + let end = (start + ALLPERMS_PAGE_SIZE).min(lines.len()); + let chunk = if start < end { &lines[start..end] } else { &[] }; + + let value = if chunk.is_empty() { + "Aucune commande.".to_string() + } else { + truncate_text(&chunk.join("\n"), 1024) + }; + + CreateEmbed::new() + .title("Permissions de toutes les commandes") + .description(format!( + "{} commande(s) · Page {}/{}", + lines.len(), + safe_page + 1, + total_pages + )) + .field("Niveaux requis", value, false) + .color(color) +} + +fn allperms_components(owner_id: UserId, page: usize, total_pages: usize) -> Vec { + let safe_total = total_pages.max(1); + let safe_page = page.min(safe_total.saturating_sub(1)); + let prev_page = safe_page.saturating_sub(1); + let next_page = (safe_page + 1).min(safe_total.saturating_sub(1)); + + vec![CreateActionRow::Buttons(vec![ + CreateButton::new(format!( + "{}:{}:{}", + ALLPERMS_CUSTOM_ID_PREFIX, + owner_id.get(), + prev_page + )) + .label("◀ Precedent") + .style(ButtonStyle::Primary) + .disabled(safe_page == 0), + CreateButton::new(format!( + "{}:{}:{}", + ALLPERMS_CUSTOM_ID_PREFIX, + owner_id.get(), + next_page + )) + .label("Suivant ▶") + .style(ButtonStyle::Primary) + .disabled(safe_page + 1 >= safe_total), + ])] +} pub async fn handle_allperms(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_allperms(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + let lines = collect_allperms_lines(ctx).await; + let total_pages = total_pages_for(lines.len()); + let requested_page = args + .first() + .and_then(|s| s.parse::().ok()) + .unwrap_or(1) + .saturating_sub(1); + let page = requested_page.min(total_pages.saturating_sub(1)); + + let color = theme_color(ctx).await; + let embed = build_allperms_embed(&lines, page, color); + let components = allperms_components(msg.author.id, page, total_pages); + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; } pub struct AllpermsCommand; @@ -13,14 +124,12 @@ pub static COMMAND_DESCRIPTOR: AllpermsCommand = AllpermsCommand; impl crate::commands::command_contract::CommandSpec for AllpermsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "allperms", - command: "allperms", + name: "allperms", category: "permissions", params: "[page]", summary: "Liste les ACL de toutes commandes", description: "Affiche le niveau ACL requis pour chaque commande avec pagination.", examples: &["+allperms", "+as", "+help allperms"], - alias_source_key: "allperms", default_aliases: &["apm"], default_permission: 8, } diff --git a/src/commands/permissions/change.rs b/src/commands/permissions/change.rs index 5890017..bcbce6f 100644 --- a/src/commands/permissions/change.rs +++ b/src/commands/permissions/change.rs @@ -1,10 +1,74 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool, normalize_command_name}; +use crate::db::{reset_command_permissions, set_command_permission}; pub async fn handle_change(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_change(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if args + .first() + .map(|s| s.eq_ignore_ascii_case("reset")) + .unwrap_or(false) + { + let removed = reset_command_permissions(&pool, bot_id).await.unwrap_or(0); + let embed = CreateEmbed::new() + .title("Permissions reinitialisees") + .description(format!("Overrides supprimes: {}", removed)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; + return; + } + + if args.len() < 2 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `change `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let command = normalize_command_name(args[0]); + let Ok(level) = args[1].parse::() else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permission invalide (0..9).") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if level > 9 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permission invalide (0..9).") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let _ = set_command_permission(&pool, bot_id, &command, level).await; + let embed = CreateEmbed::new() + .title("Permission modifiee") + .description(format!("`{}` -> niveau `{}`", command, level)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct ChangeCommand; @@ -13,14 +77,12 @@ pub static COMMAND_DESCRIPTOR: ChangeCommand = ChangeCommand; impl crate::commands::command_contract::CommandSpec for ChangeCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "change", - command: "change", + name: "change", category: "permissions", params: " | reset", summary: "Change un niveau de permission", description: "Definit le niveau ACL requis pour une commande ou reinitialise les overrides.", examples: &["+change", "+ce", "+help change"], - alias_source_key: "change", default_aliases: &["chg"], default_permission: 9, } diff --git a/src/commands/permissions/changeall.rs b/src/commands/permissions/changeall.rs index 2589576..60935c9 100644 --- a/src/commands/permissions/changeall.rs +++ b/src/commands/permissions/changeall.rs @@ -1,10 +1,77 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool}; +use crate::db::set_command_permission; +use crate::permissions::{all_command_keys, command_required_permission}; pub async fn handle_changeall(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_changeall(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + if args.len() < 2 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `changeall `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Ok(from) = args[0].parse::() else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permission source invalide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let Ok(to) = args[1].parse::() else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permission cible invalide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if from > 9 || to > 9 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permissions valides: 0..9") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let mut updated = 0usize; + for cmd in all_command_keys() { + let current = command_required_permission(ctx, &cmd).await; + if current == from { + let _ = set_command_permission(&pool, bot_id, &cmd, to).await; + updated += 1; + } + } + + let embed = CreateEmbed::new() + .title("Changeall applique") + .description(format!("{} commande(s): {} -> {}", updated, from, to)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct ChangeallCommand; @@ -13,14 +80,12 @@ pub static COMMAND_DESCRIPTOR: ChangeallCommand = ChangeallCommand; impl crate::commands::command_contract::CommandSpec for ChangeallCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "changeall", - command: "changeall", + name: "changeall", category: "permissions", params: " ", summary: "Change des permissions en masse", description: "Remplace en masse un niveau ACL source par un niveau ACL cible.", examples: &["+changeall", "+cl", "+help changeall"], - alias_source_key: "changeall", default_aliases: &["cga"], default_permission: 9, } diff --git a/src/commands/permissions/clear_perms.rs b/src/commands/permissions/clear_perms.rs index e12f5b0..aab2c7b 100644 --- a/src/commands/permissions/clear_perms.rs +++ b/src/commands/permissions/clear_perms.rs @@ -1,10 +1,32 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool}; +use crate::db::clear_role_permissions; pub async fn handle_clear_perms(ctx: &Context, msg: &Message) { - perms_service::handle_clear_perms(ctx, msg).await; + if !ensure_owner(ctx, msg).await { + return; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let removed = clear_role_permissions(&pool, bot_id).await.unwrap_or(0); + let embed = CreateEmbed::new() + .title("Permissions roles supprimees") + .description(format!("{} entree(s) supprimee(s).", removed)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct ClearPermsCommand; @@ -13,14 +35,12 @@ pub static COMMAND_DESCRIPTOR: ClearPermsCommand = ClearPermsCommand; impl crate::commands::command_contract::CommandSpec for ClearPermsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "clear_perms", - command: "clear perms", + name: "clear_perms", category: "permissions", params: "aucun", summary: "Vide toutes les permissions scope", description: "Supprime toutes les permissions ACL configurees en base.", examples: &["+clear perms", "+cs", "+help clear perms"], - alias_source_key: "clear_perms", default_aliases: &["cpm"], default_permission: 9, } diff --git a/src/commands/permissions/del.rs b/src/commands/permissions/del.rs index 670a75b..4ffa5d0 100644 --- a/src/commands/permissions/del.rs +++ b/src/commands/permissions/del.rs @@ -1,10 +1,53 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool, parse_user_or_role}; +use crate::db::remove_scope_permissions; pub async fn handle_del(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_del_perm(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + if args.len() < 2 || !args[0].eq_ignore_ascii_case("perm") { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `del perm `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some((scope_type, scope_id)) = parse_user_or_role(args[1]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Role/membre invalide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let removed = remove_scope_permissions(&pool, bot_id, scope_type, scope_id) + .await + .unwrap_or(0); + + let embed = CreateEmbed::new() + .title("Permissions supprimees") + .description(format!("{} entree(s) supprimee(s).", removed)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct DelCommand; @@ -13,14 +56,12 @@ pub static COMMAND_DESCRIPTOR: DelCommand = DelCommand; impl crate::commands::command_contract::CommandSpec for DelCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "del", - command: "del", + name: "del", category: "permissions", params: "perm <@&rôle/@membre/ID>", summary: "Supprime des permissions scope", description: "Supprime les permissions ACL associees a un role ou utilisateur.", examples: &["+del", "+dl", "+help del"], - alias_source_key: "del", default_aliases: &["dlp"], default_permission: 0, } diff --git a/src/commands/permissions/helpalias.rs b/src/commands/permissions/helpalias.rs index c9112d2..ef7156d 100644 --- a/src/commands/permissions/helpalias.rs +++ b/src/commands/permissions/helpalias.rs @@ -67,14 +67,12 @@ pub static COMMAND_DESCRIPTOR: HelpaliasCommand = HelpaliasCommand; impl crate::commands::command_contract::CommandSpec for HelpaliasCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "helpalias", - command: "helpalias", + name: "helpalias", category: "permissions", params: "", summary: "Active ou coupe les aliases help", description: "Active ou desactive laffichage des aliases dans laide.", examples: &["+helpalias", "+hs", "+help helpalias"], - alias_source_key: "helpalias", default_aliases: &["hal"], default_permission: 0, } diff --git a/src/commands/permissions/helpsetting.rs b/src/commands/permissions/helpsetting.rs index 7499541..abcb603 100644 --- a/src/commands/permissions/helpsetting.rs +++ b/src/commands/permissions/helpsetting.rs @@ -395,8 +395,7 @@ pub static COMMAND_DESCRIPTOR: HelpsettingCommand = HelpsettingCommand; impl crate::commands::command_contract::CommandSpec for HelpsettingCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "helpsetting", - command: "helpsetting", + name: "helpsetting", category: "permissions", params: "[type|aliases|perms] [value]", summary: "Configure l'affichage du système d'aide", @@ -406,7 +405,6 @@ impl crate::commands::command_contract::CommandSpec for HelpsettingCommand { "+helpsetting type hybrid", "+helpsetting perms off", ], - alias_source_key: "helpsetting", default_aliases: &["hs", "helpetting"], default_permission: 9, } diff --git a/src/commands/permissions/helptype.rs b/src/commands/permissions/helptype.rs index 8b9b562..6368bb3 100644 --- a/src/commands/permissions/helptype.rs +++ b/src/commands/permissions/helptype.rs @@ -64,14 +64,12 @@ pub static COMMAND_DESCRIPTOR: HelptypeCommand = HelptypeCommand; impl crate::commands::command_contract::CommandSpec for HelptypeCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "helptype", - command: "helptype", + name: "helptype", category: "permissions", params: "", summary: "Change le mode daffichage help", description: "Definit le mode daffichage de laide entre button, select et hybrid.", examples: &["+helptype", "+he", "+help helptype"], - alias_source_key: "helptype", default_aliases: &["htp"], default_permission: 0, } diff --git a/src/commands/permissions/mainprefix.rs b/src/commands/permissions/mainprefix.rs index baca192..4d72071 100644 --- a/src/commands/permissions/mainprefix.rs +++ b/src/commands/permissions/mainprefix.rs @@ -1,10 +1,45 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool}; +use crate::db::set_main_prefix; pub async fn handle_mainprefix(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_mainprefix(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `mainprefix `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let prefix = args[0].trim(); + if prefix.is_empty() || prefix.len() > 5 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Prefixe invalide (1 a 5 caracteres).") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + if let Some(pool) = get_pool(ctx).await { + let _ = set_main_prefix(&pool, bot_id, prefix).await; + } + + let embed = CreateEmbed::new() + .title("Prefixe principal mis a jour") + .description(format!("Nouveau prefixe principal: `{}`", prefix)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct MainprefixCommand; @@ -13,14 +48,12 @@ pub static COMMAND_DESCRIPTOR: MainprefixCommand = MainprefixCommand; impl crate::commands::command_contract::CommandSpec for MainprefixCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "mainprefix", - command: "mainprefix", + name: "mainprefix", category: "permissions", params: "", summary: "Change le prefixe global", description: "Definit le prefixe principal utilise par le bot sur tous les serveurs.", examples: &["+mainprefix", "+mx", "+help mainprefix"], - alias_source_key: "mainprefix", default_aliases: &["mpx"], default_permission: 9, } diff --git a/src/commands/permissions/perms.rs b/src/commands/permissions/perms.rs index aab31df..407e320 100644 --- a/src/commands/permissions/perms.rs +++ b/src/commands/permissions/perms.rs @@ -1,10 +1,64 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::{add_list_fields, send_embed, truncate_text}; +use crate::commands::perms_helpers::{ensure_owner, get_pool}; +use crate::db::{list_role_command_access, list_role_perm_levels, list_role_scopes}; pub async fn handle_perms(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_perms(ctx, msg, args).await; + let _ = args; + + if !ensure_owner(ctx, msg).await { + return; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let roles = list_role_scopes(&pool, bot_id).await.unwrap_or_default(); + let mut lines = Vec::new(); + + for rid in roles { + let perm_levels = list_role_perm_levels(&pool, bot_id, rid as u64) + .await + .unwrap_or_default(); + let command_access = list_role_command_access(&pool, bot_id, rid as u64) + .await + .unwrap_or_default(); + + let perms = if perm_levels.is_empty() { + "aucun".to_string() + } else { + perm_levels + .iter() + .map(|p| p.to_string()) + .collect::>() + .join(",") + }; + + let commands = if command_access.is_empty() { + "aucune".to_string() + } else { + truncate_text(&command_access.join(", "), 80) + }; + + lines.push(format!( + "<@&{}> · perms [{}] · cmd [{}]", + rid, perms, commands + )); + } + + let mut embed = CreateEmbed::new().title("Permissions du bot"); + embed = add_list_fields(embed, &lines, "Roles configures"); + send_embed(ctx, msg, embed).await; } pub struct PermsCommand; @@ -13,14 +67,12 @@ pub static COMMAND_DESCRIPTOR: PermsCommand = PermsCommand; impl crate::commands::command_contract::CommandSpec for PermsCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "perms", - command: "perms", + name: "perms", category: "permissions", params: "aucun", summary: "Affiche les permissions ACL", description: "Affiche les permissions ACL configurees par role ou scope.", examples: &["+perms", "+ps", "+help perms"], - alias_source_key: "perms", default_aliases: &["prm"], default_permission: 8, } diff --git a/src/commands/permissions/prefix.rs b/src/commands/permissions/prefix.rs index e513f6f..cc0e357 100644 --- a/src/commands/permissions/prefix.rs +++ b/src/commands/permissions/prefix.rs @@ -1,10 +1,54 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::perms_service; +use crate::commands::common::send_embed; +use crate::commands::perms_helpers::{ensure_owner, get_pool}; +use crate::db::set_guild_prefix; pub async fn handle_prefix(ctx: &Context, msg: &Message, args: &[&str]) { - perms_service::handle_prefix(ctx, msg, args).await; + if !ensure_owner(ctx, msg).await { + return; + } + + let Some(guild_id) = msg.guild_id else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Commande disponible uniquement sur un serveur.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `prefix `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let prefix = args[0].trim(); + if prefix.is_empty() || prefix.len() > 5 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Prefixe invalide (1 a 5 caracteres).") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + if let Some(pool) = get_pool(ctx).await { + let _ = set_guild_prefix(&pool, bot_id, guild_id, prefix).await; + } + + let embed = CreateEmbed::new() + .title("Prefixe serveur mis a jour") + .description(format!("Nouveau prefixe ici: `{}`", prefix)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; } pub struct PrefixCommand; @@ -13,14 +57,12 @@ pub static COMMAND_DESCRIPTOR: PrefixCommand = PrefixCommand; impl crate::commands::command_contract::CommandSpec for PrefixCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "prefix", - command: "prefix", + name: "prefix", category: "permissions", params: "", summary: "Change le prefixe serveur", description: "Definit le prefixe du serveur courant.", examples: &["+prefix", "+px", "+help prefix"], - alias_source_key: "prefix", default_aliases: &["pfx"], default_permission: 8, } diff --git a/src/commands/perms_helpers.rs b/src/commands/perms_helpers.rs new file mode 100644 index 0000000..27edd89 --- /dev/null +++ b/src/commands/perms_helpers.rs @@ -0,0 +1,58 @@ +use serenity::builder::CreateEmbed; +use serenity::model::prelude::*; +use serenity::prelude::*; +use sqlx::PgPool; + +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; +use crate::permissions::is_owner_user; + +pub fn parse_user_or_role(input: &str) -> Option<(&'static str, u64)> { + let trimmed = input.trim(); + if trimmed.starts_with("<@&") && trimmed.ends_with('>') { + return trimmed + .trim_start_matches("<@&") + .trim_end_matches('>') + .parse::() + .ok() + .map(|id| ("role", id)); + } + + if (trimmed.starts_with("<@") && trimmed.ends_with('>')) || trimmed.parse::().is_ok() { + let cleaned = trimmed + .trim_start_matches('<') + .trim_end_matches('>') + .trim_start_matches('@') + .trim_start_matches('!'); + if let Ok(id) = cleaned.parse::() { + return Some(("user", id)); + } + } + + None +} + +pub fn normalize_command_name(input: &str) -> String { + input + .trim_start_matches('+') + .replace(' ', "_") + .to_lowercase() +} + +pub async fn ensure_owner(ctx: &Context, msg: &Message) -> bool { + if is_owner_user(ctx, msg.author.id).await { + true + } else { + let embed = CreateEmbed::new() + .title("Acces refuse") + .description("Commande reservee aux owners.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + false + } +} + +pub async fn get_pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} diff --git a/src/commands/perms_service.rs b/src/commands/perms_service.rs index c556892..acc6fbe 100644 --- a/src/commands/perms_service.rs +++ b/src/commands/perms_service.rs @@ -1,536 +1,16 @@ use serenity::builder::{ CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse, - CreateInteractionResponseMessage, CreateMessage, + CreateInteractionResponseMessage, }; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::common::{add_list_fields, send_embed, theme_color, truncate_text}; -use crate::db::{ - DbPoolKey, clear_role_permissions, grant_command_access, grant_perm_level, - list_role_command_access, list_role_perm_levels, list_role_scopes, remove_scope_permissions, - reset_command_permissions, set_command_permission, set_guild_prefix, set_main_prefix, -}; -use crate::permissions::{ - all_command_keys, command_required_permission, default_permission, is_owner_user, -}; +use crate::commands::common::{theme_color, truncate_text}; +use crate::permissions::{all_command_keys, command_required_permission, default_permission}; const ALLPERMS_PAGE_SIZE: usize = 12; const ALLPERMS_CUSTOM_ID_PREFIX: &str = "allperms"; -fn parse_user_or_role(input: &str) -> Option<(&'static str, u64)> { - let trimmed = input.trim(); - if trimmed.starts_with("<@&") && trimmed.ends_with('>') { - return trimmed - .trim_start_matches("<@&") - .trim_end_matches('>') - .parse::() - .ok() - .map(|id| ("role", id)); - } - - if (trimmed.starts_with("<@") && trimmed.ends_with('>')) || trimmed.parse::().is_ok() { - let cleaned = trimmed - .trim_start_matches('<') - .trim_end_matches('>') - .trim_start_matches('@') - .trim_start_matches('!'); - if let Ok(id) = cleaned.parse::() { - return Some(("user", id)); - } - } - - None -} - -fn normalize_command_name(input: &str) -> String { - input - .trim_start_matches('+') - .replace(' ', "_") - .to_lowercase() -} - -async fn ensure_owner(ctx: &Context, msg: &Message) -> bool { - if is_owner_user(ctx, msg.author.id).await { - true - } else { - let embed = CreateEmbed::new() - .title("Accès refusé") - .description("Commande réservée aux owners.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - false - } -} - -pub async fn handle_change(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - if args - .first() - .map(|s| s.eq_ignore_ascii_case("reset")) - .unwrap_or(false) - { - let removed = reset_command_permissions(&pool, bot_id).await.unwrap_or(0); - let embed = CreateEmbed::new() - .title("Permissions réinitialisées") - .description(format!("Overrides supprimés: {}", removed)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; - return; - } - - if args.len() < 2 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `change `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let command = normalize_command_name(args[0]); - let Ok(level) = args[1].parse::() else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permission invalide (0..9).`).") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - if level > 9 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permission invalide (0..9).`).") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let _ = set_command_permission(&pool, bot_id, &command, level).await; - let embed = CreateEmbed::new() - .title("Permission modifiée") - .description(format!("`{}` -> niveau `{}`", command, level)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_changeall(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - if args.len() < 2 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `changeall `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let Ok(from) = args[0].parse::() else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permission source invalide.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - let Ok(to) = args[1].parse::() else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permission cible invalide.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - if from > 9 || to > 9 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permissions valides: 0..9") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let mut updated = 0usize; - for cmd in all_command_keys() { - let current = command_required_permission(ctx, &cmd).await; - if current == from { - let _ = set_command_permission(&pool, bot_id, &cmd, to).await; - updated += 1; - } - } - - let embed = CreateEmbed::new() - .title("Changeall appliqué") - .description(format!("{} commande(s): {} -> {}", updated, from, to)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_mainprefix(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - if args.is_empty() { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `mainprefix `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let prefix = args[0].trim(); - if prefix.is_empty() || prefix.len() > 5 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Préfixe invalide (1 à 5 caractères).`).") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - if let Some(pool) = pool { - let _ = set_main_prefix(&pool, bot_id, prefix).await; - } - - let embed = CreateEmbed::new() - .title("Préfixe principal mis à jour") - .description(format!("Nouveau préfixe principal: `{}`", prefix)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_prefix(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - let Some(guild_id) = msg.guild_id else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Commande disponible uniquement sur un serveur.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - if args.is_empty() { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `prefix `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let prefix = args[0].trim(); - if prefix.is_empty() || prefix.len() > 5 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Préfixe invalide (1 à 5 caractères).`).") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - - if let Some(pool) = pool { - let _ = set_guild_prefix(&pool, bot_id, guild_id, prefix).await; - } - - let embed = CreateEmbed::new() - .title("Préfixe serveur mis à jour") - .description(format!("Nouveau préfixe ici: `{}`", prefix)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_set_perm(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - if args.len() < 3 || !args[0].eq_ignore_ascii_case("perm") { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `set perm `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let target = parse_user_or_role(args[2]); - let Some((scope_type, scope_id)) = target else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Rôle/membre invalide.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - if let Ok(level) = args[1].parse::() { - if level > 9 { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Permission invalide (0..9).`).") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let _ = grant_perm_level(&pool, bot_id, scope_type, scope_id, level).await; - let who = if scope_type == "role" { - format!("<@&{}>", scope_id) - } else { - format!("<@{}>", scope_id) - }; - let embed = CreateEmbed::new() - .title("Permission attribuée") - .description(format!("{} reçoit la permission `{}`", who, level)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; - return; - } - - let command = normalize_command_name(args[1]); - let _ = grant_command_access(&pool, bot_id, scope_type, scope_id, &command).await; - let who = if scope_type == "role" { - format!("<@&{}>", scope_id) - } else { - format!("<@{}>", scope_id) - }; - - let embed = CreateEmbed::new() - .title("Accès commande attribué") - .description(format!("{} reçoit l'accès direct à `{}`", who, command)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_del_perm(ctx: &Context, msg: &Message, args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - if args.len() < 2 || !args[0].eq_ignore_ascii_case("perm") { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Usage: `del perm `") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - } - - let target = parse_user_or_role(args[1]); - let Some((scope_type, scope_id)) = target else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("Rôle/membre invalide.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let removed = remove_scope_permissions(&pool, bot_id, scope_type, scope_id) - .await - .unwrap_or(0); - - let embed = CreateEmbed::new() - .title("Permissions supprimées") - .description(format!("{} entrée(s) supprimée(s).", removed)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_clear_perms(ctx: &Context, msg: &Message) { - if !ensure_owner(ctx, msg).await { - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let removed = clear_role_permissions(&pool, bot_id).await.unwrap_or(0); - let embed = CreateEmbed::new() - .title("Permissions rôles supprimées") - .description(format!("{} entrée(s) supprimée(s).", removed)) - .color(0x57F287); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_perms(ctx: &Context, msg: &Message, _args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - let bot_id = ctx.cache.current_user().id; - let pool = { - let data = ctx.data.read().await; - data.get::().cloned() - }; - let Some(pool) = pool else { - let embed = CreateEmbed::new() - .title("Erreur") - .description("DB indisponible.") - .color(0xED4245); - send_embed(ctx, msg, embed).await; - return; - }; - - let roles = list_role_scopes(&pool, bot_id).await.unwrap_or_default(); - let mut lines = Vec::new(); - - for rid in roles { - let perm_levels = list_role_perm_levels(&pool, bot_id, rid as u64) - .await - .unwrap_or_default(); - let command_access = list_role_command_access(&pool, bot_id, rid as u64) - .await - .unwrap_or_default(); - - let perms = if perm_levels.is_empty() { - "aucun".to_string() - } else { - perm_levels - .iter() - .map(|p| p.to_string()) - .collect::>() - .join(",") - }; - - let commands = if command_access.is_empty() { - "aucune".to_string() - } else { - truncate_text(&command_access.join(", "), 80) - }; - - lines.push(format!( - "<@&{}> · perms [{}] · cmd [{}]", - rid, perms, commands - )); - } - - let mut embed = CreateEmbed::new().title("Permissions du bot"); - embed = add_list_fields(embed, &lines, "Rôles configurés"); - send_embed(ctx, msg, embed).await; -} - -pub async fn handle_allperms(ctx: &Context, msg: &Message, _args: &[&str]) { - if !ensure_owner(ctx, msg).await { - return; - } - - let lines = collect_allperms_lines(ctx).await; - let total_pages = total_pages_for(lines.len()); - let requested_page = _args - .first() - .and_then(|s| s.parse::().ok()) - .unwrap_or(1) - .saturating_sub(1); - let page = requested_page.min(total_pages.saturating_sub(1)); - - let color = theme_color(ctx).await; - let embed = build_allperms_embed(&lines, page, color); - let components = allperms_components(msg.author.id, page, total_pages); - - let _ = msg - .channel_id - .send_message( - &ctx.http, - CreateMessage::new().embed(embed).components(components), - ) - .await; -} - pub async fn handle_allperms_component(ctx: &Context, component: &ComponentInteraction) -> bool { let Some((owner_id, requested_page)) = parse_allperms_custom_id(&component.data.custom_id) else { @@ -588,7 +68,7 @@ async fn collect_allperms_lines(ctx: &Context) -> Vec { lines.push(format!("`{}` -> `{}`", cmd, required)); } else { lines.push(format!( - "`{}` -> `{}` (défaut `{}`)", + "`{}` -> `{}` (defaut `{}`)", cmd, required, default )); } @@ -639,7 +119,7 @@ fn allperms_components(owner_id: UserId, page: usize, total_pages: usize) -> Vec owner_id.get(), prev_page )) - .label("◀ Précédent") + .label("◀ Precedent") .style(ButtonStyle::Primary) .disabled(safe_page == 0), CreateButton::new(format!( diff --git a/src/commands/profile/compet.rs b/src/commands/profile/compet.rs index 7aade00..6ab4d45 100644 --- a/src/commands/profile/compet.rs +++ b/src/commands/profile/compet.rs @@ -1,10 +1,84 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::{RotatingActivityKind, parse_status, start_rotation}; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_compet(ctx: &Context, msg: &Message, args: &[&str]) { - botconfig_common::handle_activity(ctx, msg, "+compet", args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+playto|+listen|+watch|+compet|+stream `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(kind) = RotatingActivityKind::from_command("+compet") else { + return; + }; + + let joined = args.join(" "); + let messages: Vec = joined + .split(",,") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if messages.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Aucun message d'activité valide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + + let status = { + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { + parse_status(&saved) + } else { + OnlineStatus::Online + } + } else { + OnlineStatus::Online + } + }; + + start_rotation(ctx, kind, messages.clone(), status).await; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = + crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; + } + + let embed = CreateEmbed::new() + .title("Activité mise à jour") + .description(format!("{} message(s) configuré(s).", messages.len())) + .field( + "Rotation", + "Les textes alternent toutes les 30 secondes.", + false, + ) + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct CompetCommand; @@ -13,14 +87,12 @@ pub static COMMAND_DESCRIPTOR: CompetCommand = CompetCommand; impl crate::commands::command_contract::CommandSpec for CompetCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "compet", - command: "compet", + name: "compet", category: "profile", params: "", summary: "Definit une activite competing", description: "Configure la rotation des messages d activite en mode competing.", examples: &["+compet", "+ct", "+help compet"], - alias_source_key: "compet", default_aliases: &["cpt"], default_permission: 8, } diff --git a/src/commands/profile/discussion.rs b/src/commands/profile/discussion.rs index aa0669f..7303ea3 100644 --- a/src/commands/profile/discussion.rs +++ b/src/commands/profile/discussion.rs @@ -63,14 +63,12 @@ pub static COMMAND_DESCRIPTOR: DiscussionCommand = DiscussionCommand; impl crate::commands::command_contract::CommandSpec for DiscussionCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "discussion", - command: "discussion", + name: "discussion", category: "profile", params: " ", summary: "Diffuse un message serveur", description: "Envoie un message de discussion sur un serveur cible.", examples: &["+discussion", "+dn", "+help discussion"], - alias_source_key: "discussion", default_aliases: &["dsc"], default_permission: 8, } diff --git a/src/commands/profile/dnd.rs b/src/commands/profile/dnd.rs index bdfdeb1..a6ca4e2 100644 --- a/src/commands/profile/dnd.rs +++ b/src/commands/profile/dnd.rs @@ -1,10 +1,28 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, set_bot_status}; pub async fn handle_dnd(ctx: &Context, msg: &Message) { - botconfig_common::handle_status(ctx, msg, "+dnd").await; + ctx.dnd(); + + let bot_id = ctx.cache.current_user().id; + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let _ = set_bot_status(&pool, bot_id, "dnd").await; + } + + let embed = CreateEmbed::new() + .title("Statut mis à jour") + .description("Nouveau statut: dnd") + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct DndCommand; @@ -13,14 +31,12 @@ pub static COMMAND_DESCRIPTOR: DndCommand = DndCommand; impl crate::commands::command_contract::CommandSpec for DndCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "dnd", - command: "dnd", + name: "dnd", category: "profile", params: "aucun", summary: "Passe le bot en dnd", description: "Change le statut du bot en do not disturb et sauvegarde ce statut.", examples: &["+dnd", "+dd", "+help dnd"], - alias_source_key: "dnd", default_aliases: &["dnm"], default_permission: 8, } diff --git a/src/commands/profile/idle.rs b/src/commands/profile/idle.rs index 82f671a..97b93bc 100644 --- a/src/commands/profile/idle.rs +++ b/src/commands/profile/idle.rs @@ -1,10 +1,28 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, set_bot_status}; pub async fn handle_idle(ctx: &Context, msg: &Message) { - botconfig_common::handle_status(ctx, msg, "+idle").await; + ctx.idle(); + + let bot_id = ctx.cache.current_user().id; + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let _ = set_bot_status(&pool, bot_id, "idle").await; + } + + let embed = CreateEmbed::new() + .title("Statut mis à jour") + .description("Nouveau statut: idle") + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct IdleCommand; @@ -13,14 +31,12 @@ pub static COMMAND_DESCRIPTOR: IdleCommand = IdleCommand; impl crate::commands::command_contract::CommandSpec for IdleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "idle", - command: "idle", + name: "idle", category: "profile", params: "aucun", summary: "Passe le bot en idle", description: "Change le statut du bot en idle et sauvegarde ce statut.", examples: &["+idle", "+ie", "+help idle"], - alias_source_key: "idle", default_aliases: &["idl"], default_permission: 8, } diff --git a/src/commands/profile/invisible.rs b/src/commands/profile/invisible.rs index e42e4d8..0db8c58 100644 --- a/src/commands/profile/invisible.rs +++ b/src/commands/profile/invisible.rs @@ -1,10 +1,28 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, set_bot_status}; pub async fn handle_invisible(ctx: &Context, msg: &Message) { - botconfig_common::handle_status(ctx, msg, "+invisible").await; + ctx.invisible(); + + let bot_id = ctx.cache.current_user().id; + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let _ = set_bot_status(&pool, bot_id, "invisible").await; + } + + let embed = CreateEmbed::new() + .title("Statut mis à jour") + .description("Nouveau statut: invisible") + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct InvisibleCommand; @@ -13,14 +31,12 @@ pub static COMMAND_DESCRIPTOR: InvisibleCommand = InvisibleCommand; impl crate::commands::command_contract::CommandSpec for InvisibleCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "invisible", - command: "invisible", + name: "invisible", category: "profile", params: "aucun", summary: "Passe le bot en invisible", description: "Change le statut du bot en invisible et sauvegarde ce statut.", examples: &["+invisible", "+ie", "+help invisible"], - alias_source_key: "invisible", default_aliases: &["ivs"], default_permission: 8, } diff --git a/src/commands/profile/listen.rs b/src/commands/profile/listen.rs index dcae0bd..a9aab96 100644 --- a/src/commands/profile/listen.rs +++ b/src/commands/profile/listen.rs @@ -1,10 +1,84 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::{RotatingActivityKind, parse_status, start_rotation}; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_listen(ctx: &Context, msg: &Message, args: &[&str]) { - botconfig_common::handle_activity(ctx, msg, "+listen", args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+playto|+listen|+watch|+compet|+stream `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(kind) = RotatingActivityKind::from_command("+listen") else { + return; + }; + + let joined = args.join(" "); + let messages: Vec = joined + .split(",,") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if messages.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Aucun message d'activité valide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + + let status = { + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { + parse_status(&saved) + } else { + OnlineStatus::Online + } + } else { + OnlineStatus::Online + } + }; + + start_rotation(ctx, kind, messages.clone(), status).await; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = + crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; + } + + let embed = CreateEmbed::new() + .title("Activité mise à jour") + .description(format!("{} message(s) configuré(s).", messages.len())) + .field( + "Rotation", + "Les textes alternent toutes les 30 secondes.", + false, + ) + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct ListenCommand; @@ -13,14 +87,12 @@ pub static COMMAND_DESCRIPTOR: ListenCommand = ListenCommand; impl crate::commands::command_contract::CommandSpec for ListenCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "listen", - command: "listen", + name: "listen", category: "profile", params: "", summary: "Definit une activite listening", description: "Configure la rotation des messages d activite en mode listening.", examples: &["+listen", "+ln", "+help listen"], - alias_source_key: "listen", default_aliases: &["lsn"], default_permission: 8, } diff --git a/src/commands/profile/mp.rs b/src/commands/profile/mp.rs index 8397c61..535675b 100644 --- a/src/commands/profile/mp.rs +++ b/src/commands/profile/mp.rs @@ -460,14 +460,12 @@ pub static COMMAND_DESCRIPTOR: MpCommand = MpCommand; impl crate::commands::command_contract::CommandSpec for MpCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "mp", - command: "mp", + name: "mp", category: "profile", params: "settings [on|off] | sent [page] | delete | <@membre/ID> ", summary: "Gere lenvoi de messages prives", description: "Permet de configurer, envoyer, lister et supprimer des messages prives envoyes.", examples: &["+mp", "+help mp"], - alias_source_key: "mp", default_aliases: &["dmsg"], default_permission: 8, } diff --git a/src/commands/profile/online.rs b/src/commands/profile/online.rs index 21158fb..591c575 100644 --- a/src/commands/profile/online.rs +++ b/src/commands/profile/online.rs @@ -1,10 +1,28 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::commands::common::send_embed; +use crate::db::{DbPoolKey, set_bot_status}; pub async fn handle_online(ctx: &Context, msg: &Message) { - botconfig_common::handle_status(ctx, msg, "+online").await; + ctx.online(); + + let bot_id = ctx.cache.current_user().id; + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + if let Some(pool) = pool { + let _ = set_bot_status(&pool, bot_id, "online").await; + } + + let embed = CreateEmbed::new() + .title("Statut mis à jour") + .description("Nouveau statut: online") + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct OnlineCommand; @@ -13,14 +31,12 @@ pub static COMMAND_DESCRIPTOR: OnlineCommand = OnlineCommand; impl crate::commands::command_contract::CommandSpec for OnlineCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "online", - command: "online", + name: "online", category: "profile", params: "aucun", summary: "Passe le bot en online", description: "Change le statut du bot en online et sauvegarde ce statut.", examples: &["+online", "+oe", "+help online"], - alias_source_key: "online", default_aliases: &["onl"], default_permission: 8, } diff --git a/src/commands/profile/playto.rs b/src/commands/profile/playto.rs index 350eef7..c57690f 100644 --- a/src/commands/profile/playto.rs +++ b/src/commands/profile/playto.rs @@ -1,10 +1,84 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::{RotatingActivityKind, parse_status, start_rotation}; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_playto(ctx: &Context, msg: &Message, args: &[&str]) { - botconfig_common::handle_activity(ctx, msg, "+playto", args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+playto|+listen|+watch|+compet|+stream `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(kind) = RotatingActivityKind::from_command("+playto") else { + return; + }; + + let joined = args.join(" "); + let messages: Vec = joined + .split(",,") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if messages.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Aucun message d'activité valide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + + let status = { + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { + parse_status(&saved) + } else { + OnlineStatus::Online + } + } else { + OnlineStatus::Online + } + }; + + start_rotation(ctx, kind, messages.clone(), status).await; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = + crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; + } + + let embed = CreateEmbed::new() + .title("Activité mise à jour") + .description(format!("{} message(s) configuré(s).", messages.len())) + .field( + "Rotation", + "Les textes alternent toutes les 30 secondes.", + false, + ) + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct PlaytoCommand; @@ -13,14 +87,12 @@ pub static COMMAND_DESCRIPTOR: PlaytoCommand = PlaytoCommand; impl crate::commands::command_contract::CommandSpec for PlaytoCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "playto", - command: "playto", + name: "playto", category: "profile", params: "", summary: "Definit une activite playing", description: "Configure la rotation des messages d activite en mode playing.", examples: &["+playto", "+po", "+help playto"], - alias_source_key: "playto", default_aliases: &["ply"], default_permission: 8, } diff --git a/src/commands/profile/remove_activity.rs b/src/commands/profile/remove_activity.rs index e5772cc..c9741fa 100644 --- a/src/commands/profile/remove_activity.rs +++ b/src/commands/profile/remove_activity.rs @@ -1,10 +1,31 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::stop_rotation; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_remove_activity(ctx: &Context, msg: &Message) { - botconfig_common::handle_remove_activity(ctx, msg).await; + stop_rotation(ctx).await; + ctx.set_activity(None); + + let bot_id = ctx.cache.current_user().id; + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = crate::db::clear_bot_activity(&pool, bot_id).await; + } + + let embed = CreateEmbed::new() + .title("Activité supprimée") + .description("L'activité du bot a été retirée.") + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct RemoveActivityCommand; @@ -13,14 +34,12 @@ pub static COMMAND_DESCRIPTOR: RemoveActivityCommand = RemoveActivityCommand; impl crate::commands::command_contract::CommandSpec for RemoveActivityCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "remove_activity", - command: "remove activity", + name: "remove_activity", category: "profile", params: "aucun", summary: "Supprime lactivite du bot", description: "Arrete la rotation d activite et retire lactivite courante du bot.", examples: &["+remove activity", "+ry", "+help remove activity"], - alias_source_key: "remove_activity", default_aliases: &["rma"], default_permission: 8, } diff --git a/src/commands/profile/set.rs b/src/commands/profile/set.rs index cb7c563..36e6bff 100644 --- a/src/commands/profile/set.rs +++ b/src/commands/profile/set.rs @@ -3,7 +3,83 @@ use serenity::model::prelude::*; use serenity::prelude::*; use crate::commands::common::send_embed; -use crate::commands::perms_service; +use crate::commands::perms_helpers::{ + ensure_owner, get_pool, normalize_command_name, parse_user_or_role, +}; +use crate::db::{grant_command_access, grant_perm_level}; + +async fn handle_set_perm(ctx: &Context, msg: &Message, args: &[&str]) { + if !ensure_owner(ctx, msg).await { + return; + } + + if args.len() < 3 || !args[0].eq_ignore_ascii_case("perm") { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `set perm `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some((scope_type, scope_id)) = parse_user_or_role(args[2]) else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Role/membre invalide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + let embed = CreateEmbed::new() + .title("Erreur") + .description("DB indisponible.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + }; + + if let Ok(level) = args[1].parse::() { + if level > 9 { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Permission invalide (0..9).") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let _ = grant_perm_level(&pool, bot_id, scope_type, scope_id, level).await; + let who = if scope_type == "role" { + format!("<@&{}>", scope_id) + } else { + format!("<@{}>", scope_id) + }; + let embed = CreateEmbed::new() + .title("Permission attribuee") + .description(format!("{} recoit la permission `{}`", who, level)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; + return; + } + + let command = normalize_command_name(args[1]); + let _ = grant_command_access(&pool, bot_id, scope_type, scope_id, &command).await; + + let who = if scope_type == "role" { + format!("<@&{}>", scope_id) + } else { + format!("<@{}>", scope_id) + }; + + let embed = CreateEmbed::new() + .title("Acces commande attribue") + .description(format!("{} recoit l'acces direct a `{}`", who, command)) + .color(0x57F287); + send_embed(ctx, msg, embed).await; +} pub async fn handle_set(ctx: &Context, msg: &Message, args: &[&str]) { if args @@ -11,7 +87,7 @@ pub async fn handle_set(ctx: &Context, msg: &Message, args: &[&str]) { .map(|a| a.eq_ignore_ascii_case("perm")) .unwrap_or(false) { - perms_service::handle_set_perm(ctx, msg, args).await; + handle_set_perm(ctx, msg, args).await; return; } @@ -245,14 +321,12 @@ pub static COMMAND_DESCRIPTOR: SetCommand = SetCommand; impl crate::commands::command_contract::CommandSpec for SetCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "set", - command: "set", + name: "set", category: "profile", params: "name | pic | banner | profil ;; ;; | perm ...", summary: "Configure le profil du bot", description: "Modifie le nom, lavatar, la banniere ou des options avancees via les sous commandes.", examples: &["+set", "+st", "+help set"], - alias_source_key: "set", default_aliases: &["cfg"], default_permission: 8, } diff --git a/src/commands/profile/stream.rs b/src/commands/profile/stream.rs index 0c73fdb..5435ec8 100644 --- a/src/commands/profile/stream.rs +++ b/src/commands/profile/stream.rs @@ -1,10 +1,84 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::{RotatingActivityKind, parse_status, start_rotation}; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_stream(ctx: &Context, msg: &Message, args: &[&str]) { - botconfig_common::handle_activity(ctx, msg, "+stream", args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+playto|+listen|+watch|+compet|+stream `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(kind) = RotatingActivityKind::from_command("+stream") else { + return; + }; + + let joined = args.join(" "); + let messages: Vec = joined + .split(",,") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if messages.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Aucun message d'activité valide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + + let status = { + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { + parse_status(&saved) + } else { + OnlineStatus::Online + } + } else { + OnlineStatus::Online + } + }; + + start_rotation(ctx, kind, messages.clone(), status).await; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = + crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; + } + + let embed = CreateEmbed::new() + .title("Activité mise à jour") + .description(format!("{} message(s) configuré(s).", messages.len())) + .field( + "Rotation", + "Les textes alternent toutes les 30 secondes.", + false, + ) + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct StreamCommand; @@ -13,14 +87,12 @@ pub static COMMAND_DESCRIPTOR: StreamCommand = StreamCommand; impl crate::commands::command_contract::CommandSpec for StreamCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "stream", - command: "stream", + name: "stream", category: "profile", params: "", summary: "Definit une activite streaming", description: "Configure la rotation des messages d activite en mode streaming.", examples: &["+stream", "+sm", "+help stream"], - alias_source_key: "stream", default_aliases: &["stm"], default_permission: 8, } diff --git a/src/commands/profile/theme.rs b/src/commands/profile/theme.rs index 01d945f..4ccf0c3 100644 --- a/src/commands/profile/theme.rs +++ b/src/commands/profile/theme.rs @@ -50,14 +50,12 @@ pub static COMMAND_DESCRIPTOR: ThemeCommand = ThemeCommand; impl crate::commands::command_contract::CommandSpec for ThemeCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "theme", - command: "theme", + name: "theme", category: "profile", params: "", summary: "Definit la couleur du theme", description: "Met a jour la couleur principale des embeds du bot.", examples: &["+theme", "+te", "+help theme"], - alias_source_key: "theme", default_aliases: &["thm"], default_permission: 8, } diff --git a/src/commands/profile/watch.rs b/src/commands/profile/watch.rs index 9fe07d9..175856e 100644 --- a/src/commands/profile/watch.rs +++ b/src/commands/profile/watch.rs @@ -1,10 +1,84 @@ +use serenity::builder::CreateEmbed; use serenity::model::prelude::*; use serenity::prelude::*; -use crate::commands::botconfig_common; +use crate::activity::{RotatingActivityKind, parse_status, start_rotation}; +use crate::commands::common::send_embed; +use crate::db::DbPoolKey; pub async fn handle_watch(ctx: &Context, msg: &Message, args: &[&str]) { - botconfig_common::handle_activity(ctx, msg, "+watch", args).await; + if args.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Usage: `+playto|+listen|+watch|+compet|+stream `") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let Some(kind) = RotatingActivityKind::from_command("+watch") else { + return; + }; + + let joined = args.join(" "); + let messages: Vec = joined + .split(",,") + .map(|s| s.trim()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + + if messages.is_empty() { + let embed = CreateEmbed::new() + .title("Erreur") + .description("Aucun message d'activité valide.") + .color(0xED4245); + send_embed(ctx, msg, embed).await; + return; + } + + let bot_id = ctx.cache.current_user().id; + + let status = { + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await { + parse_status(&saved) + } else { + OnlineStatus::Online + } + } else { + OnlineStatus::Online + } + }; + + start_rotation(ctx, kind, messages.clone(), status).await; + + let pool = { + let data = ctx.data.read().await; + data.get::().cloned() + }; + + if let Some(pool) = pool { + let _ = + crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await; + } + + let embed = CreateEmbed::new() + .title("Activité mise à jour") + .description(format!("{} message(s) configuré(s).", messages.len())) + .field( + "Rotation", + "Les textes alternent toutes les 30 secondes.", + false, + ) + .color(0x57F287); + + send_embed(ctx, msg, embed).await; } pub struct WatchCommand; @@ -13,14 +87,12 @@ pub static COMMAND_DESCRIPTOR: WatchCommand = WatchCommand; impl crate::commands::command_contract::CommandSpec for WatchCommand { fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { crate::commands::command_contract::CommandMetadata { - key: "watch", - command: "watch", + name: "watch", category: "profile", params: "", summary: "Definit une activite watching", description: "Configure la rotation des messages d activite en mode watching.", examples: &["+watch", "+wh", "+help watch"], - alias_source_key: "watch", default_aliases: &["wtc"], default_permission: 8, } diff --git a/src/events/interaction_create_event.rs b/src/events/interaction_create_event.rs index 2adba48..1d94bed 100644 --- a/src/events/interaction_create_event.rs +++ b/src/events/interaction_create_event.rs @@ -2,8 +2,7 @@ use serenity::model::prelude::*; use serenity::prelude::*; use crate::commands::{ - advanced_tools, boostembed, help, helpsetting, mp, perms_service, suggestion, tempvoc, - ticket, + advanced_tools, boostembed, help, helpsetting, mp, perms_service, suggestion, tempvoc, ticket, }; pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) {