diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e73322f --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2026 Arthur + +Licence pour tous les projets Arthur + +1. Définition +Cette licence définit les droits et obligations concernant l'utilisation, la modification et la redistribution du code fourni par l'auteur. + +2. Autorisation d'utilisation +Vous êtes libre d'utiliser ce code pour vos projets personnels ou commerciaux. L'utilisation doit inclure une mention de l'auteur d’une manière libre (ex: "inspiré de ArthurP"). + +3. Modification +Vous pouvez modifier, adapter ou améliorer le code pour vos besoins. Les modifications doivent être identifiées comme telles et ne doivent pas être présentées comme l'original. + +4. Redistribution +- Le code original **ne peut pas être redistribué tel quel**. +- Les versions modifiées peuvent être partagées, sous réserve de mentionner l'auteur original. + +5. Usage commercial +L’usage commercial des versions modifiées est autorisé. Vous pouvez générer des revenus avec votre version modifiée. + +6. Attribution +L'auteur original doit être cité d’une manière libre, mais visible, sur tout projet utilisant ce code ou ses dérivés. + +7. Responsabilité +Le code est fourni "tel quel", sans garantie d’aucune sorte. L’auteur décline toute responsabilité pour tout dommage direct ou indirect résultant de l’utilisation du code. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dfa6ab --- /dev/null +++ b/README.md @@ -0,0 +1,178 @@ +# Shadowbot + +Bot Discord ecrit en Rust (Serenity) avec une architecture modulaire, des commandes prefixees, des interactions (slash/components/modals) et une couche PostgreSQL pour la persistance. + +Le projet couvre moderation, logs, tickets, suggestions, automatisations, gestion des roles/salons, jeux et configuration fine des permissions. + +## Points forts + +- 200+ commandes (prefixees + alias) +- Gestion de permissions par commande et par niveau +- Prefixe principal + prefixes par serveur +- Systeme de tickets avec claim/close/add/remove +- Suggestions avec workflow d approbation +- Automod (antispam, antilink, badwords, antimassmention) +- Logs (messages, moderation, vocal, roles, boosts, raids) +- Presence dynamique (play/listen/watch/compet/stream) +- Persistance PostgreSQL (schema cree automatiquement au demarrage) + +## Stack technique + +- Rust edition 2024 +- [Serenity 0.12](https://github.com/serenity-rs/serenity) +- Tokio +- SQLx + PostgreSQL +- dotenv +- Docker / Docker Compose + +## Structure du projet + +```text +src/ + main.rs # bootstrap bot + DB + permissions.rs # ACL, prefixes, checks permissions + db.rs # pool SQLx + schema + acces DB + activity.rs # rotation de presence/statut + commands/ # commandes prefixees par domaine + events/ # handlers d evenements Discord + utils/ # services transverses (logs, automod, etc.) +``` + +## Prerequis + +- Rust stable (toolchain recente compatible edition 2024) +- Cargo +- PostgreSQL (optionnel, mais recommande) +- Un bot Discord et son token + +Important: le bot utilise `GatewayIntents::all()`. Pense a activer les intents necessaires dans le portail Discord Developer (dont Message Content intent). + +## Configuration + +1. Copier le fichier d exemple: + +```bash +cp .env.example .env +``` + +2. Renseigner les variables dans `.env`: + +```env +# Discord +BOT_TOKEN=change_me +FORCE_OWNER_IDS=671763971803447298 + +# PostgreSQL +POSTGRES_DB=shadowbot +POSTGRES_USER=shadowbot +POSTGRES_PASSWORD=change_me + +# App database URL +DATABASE_URL=postgres://shadowbot:change_me@postgres:5432/shadowbot +``` + +### Variables importantes + +- `BOT_TOKEN`: token du bot Discord (obligatoire) +- `FORCE_OWNER_IDS`: IDs utilisateurs consideres owners (CSV possible) +- `DATABASE_URL`: URL PostgreSQL de l application + +Note: si `DATABASE_URL` pointe vers `@postgres:` mais que le bot tourne hors Docker, le code tente un fallback automatique vers `@localhost:`. + +## Lancement en local (sans Docker pour le bot) + +1. Demarrer PostgreSQL (local ou via Docker). + +2. Si besoin, lancer seulement la base via Compose: + +```bash +docker compose up -d postgres +``` + +3. Lancer le bot: + +```bash +cargo run +``` + +Le schema SQL est initialise automatiquement au demarrage. + +## Lancement full Docker (bot + base) + +```bash +docker compose up --build -d +``` + +Voir les logs: + +```bash +docker compose logs -f bot +``` + +Arreter: + +```bash +docker compose down +``` + +## Fonctionnement des commandes + +- Prefixe par defaut: `+` +- Prefixe principal modifiable: `+mainprefix ` +- Prefixe serveur modifiable: `+prefix ` +- Aide: `+help` ou `/help` + +Exemples: + +```text ++ping ++help moderation ++ticket ++suggestion ++warn @user raison +``` + +### Slash commands + +Le projet supporte les interactions slash/components/modals. La commande `/help` est enregistree globalement au demarrage, et plusieurs modules gerent aussi des interactions slash specifiques (ticket, suggestions, tempvoc, autopublish, etc.). + +## Base de donnees + +Le schema est cree dans `src/db.rs` via `init_schema` avec plusieurs tables, notamment: + +- `message_log` +- `bot_settings`, `bot_activities` +- `bot_command_permissions`, `bot_command_access`, `bot_perm_level_access` +- `bot_aliases` +- `bot_tickets`, `bot_ticket_members`, `bot_ticket_settings` +- `bot_suggestions`, `bot_suggestion_settings` +- `bot_autopublish_channels`, `bot_piconly_channels` +- `bot_moderation_settings`, `bot_badwords`, `bot_strike_rules`, `bot_punish_rules` +- `bot_game_sessions` + +Si `DATABASE_URL` est absent ou invalide, le bot demarre quand meme, mais certaines fonctions persistantes sont desactivees (ex: snipe persistant). + +## Verification rapide + +```bash +cargo fmt +cargo check +``` + +## Contribution + +1. Ajouter/modifier un module de commande dans `src/commands/...` +2. Declarer le module dans `src/commands/mod.rs` +3. Ajouter son `COMMAND_DESCRIPTOR` (metadata) +4. Router la commande dans `src/events/message.rs` +5. Si interaction: router explicitement dans `src/events/interaction_create.rs` +6. Verifier ACL/permissions et aliases + +## Securite + +- Ne jamais commit de secrets (`.env` est ignore) +- Utiliser `.env.example` pour partager la config type + +## Licence + +Le projet est distribue sous une licence personnalisee. Voir le fichier `LICENSE`. diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 68abb4e..cafbeeb 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -262,6 +262,8 @@ pub mod perms; pub mod perms_helpers; #[path = "../utils/perms_service.rs"] pub mod perms_service; +#[path = "perms/permsmenu.rs"] +pub mod permsmenu; #[path = "game/pfc.rs"] pub mod pfc; #[path = "info/pic.rs"] @@ -630,6 +632,7 @@ pub fn all_command_metadata() -> Vec { mainprefix::COMMAND_DESCRIPTOR.metadata(), prefix::COMMAND_DESCRIPTOR.metadata(), perms::COMMAND_DESCRIPTOR.metadata(), + permsmenu::COMMAND_DESCRIPTOR.metadata(), delperm::COMMAND_DESCRIPTOR.metadata(), clear_perms::COMMAND_DESCRIPTOR.metadata(), allperms::COMMAND_DESCRIPTOR.metadata(), diff --git a/src/commands/perms/help.rs b/src/commands/perms/help.rs index 9778e30..9a25eda 100644 --- a/src/commands/perms/help.rs +++ b/src/commands/perms/help.rs @@ -163,9 +163,8 @@ fn help_page_for_command( "owner" | "unowner" | "clearowners" | "bl" | "unbl" | "blinfo" | "clearbl" | "allbots" | "alladmins" | "botadmins" | "mainprefix" | "prefix" | "mp" | "mpsettings" | "mpsent" | "mpdelete" | "leave" | "discussion" => "administration", - "perms" | "delperm" | "clearperms" | "allperms" | "alias" | "help" | "helpsetting" => { - "permissions" - } + "perms" | "permsmenu" | "delperm" | "clearperms" | "allperms" | "alias" | "help" + | "helpsetting" => "permissions", _ => match meta.category { "info" => "infos", "invitation" => "invitation", @@ -467,6 +466,7 @@ fn help_lookup_to_key(input: &str) -> Option<&'static str> { "mainprefix" => Some("mainprefix"), "prefix" => Some("prefix"), "perms" => Some("perms"), + "permsmenu" | "perms menu" => Some("permsmenu"), "allperms" => Some("allperms"), "setperm" => Some("setperm"), "delperm" => Some("delperm"), diff --git a/src/commands/perms/permsmenu.rs b/src/commands/perms/permsmenu.rs new file mode 100644 index 0000000..e4ffaec --- /dev/null +++ b/src/commands/perms/permsmenu.rs @@ -0,0 +1,775 @@ +use serenity::builder::{ + CreateActionRow, CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse, + CreateInteractionResponseMessage, CreateMessage, CreateModal, CreateSelectMenu, + CreateSelectMenuKind, CreateSelectMenuOption, +}; +use serenity::model::application::{ + ActionRowComponent, ButtonStyle, ComponentInteraction, ComponentInteractionDataKind, + InputTextStyle, ModalInteraction, +}; +use serenity::model::prelude::*; +use serenity::prelude::*; + +use crate::commands::common::{send_embed, theme_color, truncate_text}; +use crate::commands::perms_helpers::{ + ensure_owner, get_pool, normalize_command_name, parse_user_or_role, +}; +use crate::db::{ + clear_role_permissions, grant_command_access, grant_perm_level, list_role_scopes, + remove_scope_permissions, reset_command_permissions, set_command_permission, +}; +use crate::permissions::{all_command_keys, command_required_permission, default_permission}; + +const PERMSMENU_PREFIX: &str = "permsmenu"; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum MenuView { + Overview, + Overrides, + Usage, +} + +impl MenuView { + fn from_action(action: &str) -> Option { + match action { + "overview" => Some(Self::Overview), + "overrides" => Some(Self::Overrides), + "usage" => Some(Self::Usage), + _ => None, + } + } +} + +#[derive(Default)] +struct AclSnapshot { + total_commands: usize, + overridden_commands: usize, + role_scopes: usize, + overridden_lines: Vec, +} + +fn parse_component_custom_id(custom_id: &str) -> Option<(String, String, u64)> { + let mut parts = custom_id.split(':'); + if parts.next()? != PERMSMENU_PREFIX { + return None; + } + + let group = parts.next()?.to_string(); + let action = parts.next()?.to_string(); + let owner_id = parts.next()?.parse::().ok()?; + + if parts.next().is_some() { + return None; + } + + Some((group, action, owner_id)) +} + +fn parse_modal_custom_id(custom_id: &str) -> Option<(String, u64)> { + let mut parts = custom_id.split(':'); + if parts.next()? != PERMSMENU_PREFIX { + return None; + } + if parts.next()? != "submit" { + return None; + } + + let action = parts.next()?.to_string(); + let owner_id = parts.next()?.parse::().ok()?; + + if parts.next().is_some() { + return None; + } + + Some((action, owner_id)) +} + +fn parse_quick_value(value: &str) -> Option<(String, String)> { + let mut parts = value.split(':'); + let group = parts.next()?.to_string(); + let action = parts.next()?.to_string(); + + if parts.next().is_some() { + return None; + } + + Some((group, action)) +} + +fn modal_value(modal: &ModalInteraction, wanted_id: &str) -> Option { + for row in &modal.data.components { + for component in &row.components { + if let ActionRowComponent::InputText(input) = component { + if input.custom_id == wanted_id { + return input.value.clone(); + } + } + } + } + + None +} + +fn scope_mention(scope_type: &str, scope_id: u64) -> String { + if scope_type == "role" { + format!("<@&{}>", scope_id) + } else { + format!("<@{}>", scope_id) + } +} + +async fn component_ephemeral(ctx: &Context, component: &ComponentInteraction, content: &str) { + let _ = component + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content(content) + .ephemeral(true), + ), + ) + .await; +} + +async fn modal_ephemeral(ctx: &Context, modal: &ModalInteraction, content: &str) { + let _ = modal + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content(content) + .ephemeral(true), + ), + ) + .await; +} + +async fn acl_snapshot(ctx: &Context, pool: &sqlx::PgPool, bot_id: UserId) -> AclSnapshot { + let mut commands = all_command_keys(); + commands.sort(); + + let mut overridden_lines = Vec::new(); + for command in &commands { + let required = command_required_permission(ctx, command).await; + let default = default_permission(command); + + if required != default { + overridden_lines.push(format!( + "`{}` -> `{}` (defaut `{}`)", + command, required, default + )); + } + } + + let role_scopes = list_role_scopes(pool, bot_id) + .await + .unwrap_or_default() + .len(); + + AclSnapshot { + total_commands: commands.len(), + overridden_commands: overridden_lines.len(), + role_scopes, + overridden_lines, + } +} + +fn build_overview_embed( + snapshot: &AclSnapshot, + color: u32, + last_action: Option<&str>, +) -> CreateEmbed { + let mut embed = CreateEmbed::new() + .title("Perms Menu v2") + .description("Panel ACL v2 pour configurer commandes, niveaux et scopes depuis des composants message.") + .field( + "Etat ACL", + format!( + "Commandes connues: `{}`\nOverrides commandes: `{}`\nScopes roles configures: `{}`", + snapshot.total_commands, snapshot.overridden_commands, snapshot.role_scopes + ), + false, + ) + .field( + "Actions", + "- Set command permission\n- Grant permission or command access\n- Delete scope ACL\n- Reset command overrides\n- Clear role ACL", + false, + ) + .field( + "Raccourcis", + "`+allperms` `+perms` `+setperm` `+delperm` `+clearperms`", + false, + ) + .color(color); + + if let Some(action) = last_action { + embed = embed.field("Derniere action", action, false); + } + + embed +} + +fn build_overrides_embed(snapshot: &AclSnapshot, color: u32) -> CreateEmbed { + let details = if snapshot.overridden_lines.is_empty() { + "Aucun override de commande actif.".to_string() + } else { + truncate_text(&snapshot.overridden_lines.join("\n"), 3800) + }; + + CreateEmbed::new() + .title("Perms Menu v2 - Overrides") + .description(format!( + "{} override(s) detecte(s).", + snapshot.overridden_commands + )) + .field("Liste", details, false) + .color(color) +} + +fn build_usage_embed(color: u32) -> CreateEmbed { + CreateEmbed::new() + .title("Perms Menu v2 - Aide") + .description("Syntaxes utiles pour piloter le systeme ACL.") + .field( + "Commandes", + "`+setperm 6 @Role`\n`+setperm mute @Role`\n`+delperm @Role`\n`+clearperms`\n`+allperms`", + false, + ) + .field( + "Depuis le panel", + "- Boutons: ouvrir modals et actions directes\n- Select: navigation rapide et operations", + false, + ) + .color(color) +} + +fn build_components(owner_id: UserId, view: MenuView) -> Vec { + let overview_style = if view == MenuView::Overview { + ButtonStyle::Success + } else { + ButtonStyle::Secondary + }; + let overrides_style = if view == MenuView::Overrides { + ButtonStyle::Success + } else { + ButtonStyle::Secondary + }; + let usage_style = if view == MenuView::Usage { + ButtonStyle::Success + } else { + ButtonStyle::Secondary + }; + + let row_actions = CreateActionRow::Buttons(vec![ + CreateButton::new(format!( + "{}:modal:setcmd:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Set Command") + .style(ButtonStyle::Primary), + CreateButton::new(format!( + "{}:modal:grant:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Grant Scope") + .style(ButtonStyle::Primary), + CreateButton::new(format!("{}:modal:del:{}", PERMSMENU_PREFIX, owner_id.get())) + .label("Delete Scope") + .style(ButtonStyle::Danger), + CreateButton::new(format!( + "{}:view:overview:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Dashboard") + .style(overview_style), + ]); + + let row_views = CreateActionRow::Buttons(vec![ + CreateButton::new(format!( + "{}:view:overrides:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Overrides") + .style(overrides_style), + CreateButton::new(format!( + "{}:view:usage:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Usage") + .style(usage_style), + CreateButton::new(format!( + "{}:apply:resetcmd:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Reset Cmd") + .style(ButtonStyle::Danger), + CreateButton::new(format!( + "{}:apply:clearroles:{}", + PERMSMENU_PREFIX, + owner_id.get() + )) + .label("Clear Roles") + .style(ButtonStyle::Danger), + ]); + + let quick_menu = CreateSelectMenu::new( + format!("{}:quick:actions:{}", PERMSMENU_PREFIX, owner_id.get()), + CreateSelectMenuKind::String { + options: vec![ + CreateSelectMenuOption::new("View dashboard", "view:overview"), + CreateSelectMenuOption::new("View overrides", "view:overrides"), + CreateSelectMenuOption::new("View usage", "view:usage"), + CreateSelectMenuOption::new("Apply reset command overrides", "apply:resetcmd"), + CreateSelectMenuOption::new("Apply clear role ACL", "apply:clearroles"), + ], + }, + ) + .placeholder("Quick action ACL v2"); + + vec![ + row_actions, + row_views, + CreateActionRow::SelectMenu(quick_menu), + ] +} + +fn set_command_modal(owner_id: u64) -> CreateModal { + CreateModal::new( + format!("{}:submit:setcmd:{}", PERMSMENU_PREFIX, owner_id), + "Perms Menu v2 - Set Command", + ) + .components(vec![ + CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Command key", "command") + .placeholder("mute, clearperms, ticketadd") + .required(true), + ), + CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Permission level (0-9)", "level") + .placeholder("6") + .required(true), + ), + ]) +} + +fn grant_scope_modal(owner_id: u64) -> CreateModal { + CreateModal::new( + format!("{}:submit:grant:{}", PERMSMENU_PREFIX, owner_id), + "Perms Menu v2 - Grant Scope", + ) + .components(vec![ + CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Mode (level|command)", "mode") + .placeholder("level") + .required(false), + ), + CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Value (0-9 or command)", "value") + .placeholder("6 or mute") + .required(true), + ), + CreateActionRow::InputText( + CreateInputText::new( + InputTextStyle::Short, + "Target (role/user mention or id)", + "target", + ) + .placeholder("<@&123> or <@123>") + .required(true), + ), + ]) +} + +fn delete_scope_modal(owner_id: u64) -> CreateModal { + CreateModal::new( + format!("{}:submit:del:{}", PERMSMENU_PREFIX, owner_id), + "Perms Menu v2 - Delete Scope", + ) + .components(vec![CreateActionRow::InputText( + CreateInputText::new( + InputTextStyle::Short, + "Target (role/user mention or id)", + "target", + ) + .placeholder("<@&123> or <@123>") + .required(true), + )]) +} + +async fn update_panel_message( + ctx: &Context, + component: &ComponentInteraction, + pool: &sqlx::PgPool, + bot_id: UserId, + view: MenuView, + last_action: Option<&str>, +) { + let snapshot = acl_snapshot(ctx, pool, bot_id).await; + let color = theme_color(ctx).await; + + let embed = match view { + MenuView::Overview => build_overview_embed(&snapshot, color, last_action), + MenuView::Overrides => build_overrides_embed(&snapshot, color), + MenuView::Usage => build_usage_embed(color), + }; + + let _ = component + .create_response( + &ctx.http, + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::new() + .embed(embed) + .components(build_components(component.user.id, view)), + ), + ) + .await; +} + +async fn apply_set_command_modal( + modal: &ModalInteraction, + pool: &sqlx::PgPool, + bot_id: UserId, +) -> Result { + let command_raw = modal_value(modal, "command") + .unwrap_or_default() + .trim() + .to_string(); + if command_raw.is_empty() { + return Err("Commande manquante.".to_string()); + } + + let level = modal_value(modal, "level") + .unwrap_or_default() + .trim() + .parse::() + .map_err(|_| "Niveau invalide. Valeurs attendues: 0..9".to_string())?; + if level > 9 { + return Err("Niveau invalide. Valeurs attendues: 0..9".to_string()); + } + + let command = normalize_command_name(&command_raw); + let known_commands = all_command_keys(); + if !known_commands.iter().any(|cmd| cmd == &command) { + return Err(format!("Commande inconnue: `{}`", command)); + } + + set_command_permission(pool, bot_id, &command, level) + .await + .map_err(|err| format!("Erreur DB: {}", err))?; + + Ok(format!( + "Permission requise pour `{}` definie sur `{}`.", + command, level + )) +} + +async fn apply_grant_modal( + modal: &ModalInteraction, + pool: &sqlx::PgPool, + bot_id: UserId, +) -> Result { + let mode = modal_value(modal, "mode") + .unwrap_or_default() + .trim() + .to_lowercase(); + let value = modal_value(modal, "value") + .unwrap_or_default() + .trim() + .to_string(); + let target = modal_value(modal, "target") + .unwrap_or_default() + .trim() + .to_string(); + + if value.is_empty() || target.is_empty() { + return Err("Value et target sont obligatoires.".to_string()); + } + + let Some((scope_type, scope_id)) = parse_user_or_role(&target) else { + return Err("Target invalide. Utilise une mention role/user ou un id.".to_string()); + }; + + let is_level_mode = matches!(mode.as_str(), "level" | "perm" | "permission" | "niveau") + || (mode.is_empty() && value.parse::().is_ok()); + + if is_level_mode { + let level = value + .parse::() + .map_err(|_| "Niveau invalide. Valeurs attendues: 0..9".to_string())?; + if level > 9 { + return Err("Niveau invalide. Valeurs attendues: 0..9".to_string()); + } + + grant_perm_level(pool, bot_id, scope_type, scope_id, level) + .await + .map_err(|err| format!("Erreur DB: {}", err))?; + + return Ok(format!( + "Permission `{}` accordee a {}.", + level, + scope_mention(scope_type, scope_id) + )); + } + + let command = normalize_command_name(&value); + let known_commands = all_command_keys(); + if !known_commands.iter().any(|cmd| cmd == &command) { + return Err(format!("Commande inconnue: `{}`", command)); + } + + grant_command_access(pool, bot_id, scope_type, scope_id, &command) + .await + .map_err(|err| format!("Erreur DB: {}", err))?; + + Ok(format!( + "Acces direct `{}` accorde a {}.", + command, + scope_mention(scope_type, scope_id) + )) +} + +async fn apply_delete_modal( + modal: &ModalInteraction, + pool: &sqlx::PgPool, + bot_id: UserId, +) -> Result { + let target = modal_value(modal, "target") + .unwrap_or_default() + .trim() + .to_string(); + + let Some((scope_type, scope_id)) = parse_user_or_role(&target) else { + return Err("Target invalide. Utilise une mention role/user ou un id.".to_string()); + }; + + let removed = remove_scope_permissions(pool, bot_id, scope_type, scope_id) + .await + .map_err(|err| format!("Erreur DB: {}", err))?; + + Ok(format!( + "{} entree(s) ACL supprimee(s) pour {}.", + removed, + scope_mention(scope_type, scope_id) + )) +} + +pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool { + let Some((group, action, owner_id)) = parse_component_custom_id(&component.data.custom_id) + else { + return false; + }; + + if component.user.id.get() != owner_id { + component_ephemeral( + ctx, + component, + "Seul l'auteur de la commande peut utiliser ce panel.", + ) + .await; + return true; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + component_ephemeral(ctx, component, "DB indisponible.").await; + return true; + }; + + if group == "modal" { + let modal = match action.as_str() { + "setcmd" => Some(set_command_modal(owner_id)), + "grant" => Some(grant_scope_modal(owner_id)), + "del" => Some(delete_scope_modal(owner_id)), + _ => None, + }; + + if let Some(modal) = modal { + let _ = component + .create_response(&ctx.http, CreateInteractionResponse::Modal(modal)) + .await; + return true; + } + + return false; + } + + if group == "quick" { + if let ComponentInteractionDataKind::StringSelect { values } = &component.data.kind { + if let Some(selected) = values.first() { + if let Some((quick_group, quick_action)) = parse_quick_value(selected) { + if quick_group == "view" { + if let Some(view) = MenuView::from_action(&quick_action) { + update_panel_message(ctx, component, &pool, bot_id, view, None).await; + return true; + } + } + + if quick_group == "apply" { + let status = match quick_action.as_str() { + "resetcmd" => { + let removed = + reset_command_permissions(&pool, bot_id).await.unwrap_or(0); + format!( + "Reset des permissions de commande termine: {} entree(s).", + removed + ) + } + "clearroles" => { + let removed = + clear_role_permissions(&pool, bot_id).await.unwrap_or(0); + format!("Reset ACL roles termine: {} entree(s).", removed) + } + _ => "Action inconnue.".to_string(), + }; + + update_panel_message( + ctx, + component, + &pool, + bot_id, + MenuView::Overview, + Some(&status), + ) + .await; + return true; + } + } + } + } + + component_ephemeral(ctx, component, "Action rapide invalide.").await; + return true; + } + + if group == "view" { + if let Some(view) = MenuView::from_action(&action) { + update_panel_message(ctx, component, &pool, bot_id, view, None).await; + return true; + } + + component_ephemeral(ctx, component, "Vue inconnue.").await; + return true; + } + + if group == "apply" { + let status = match action.as_str() { + "resetcmd" => { + let removed = reset_command_permissions(&pool, bot_id).await.unwrap_or(0); + format!( + "Reset des permissions de commande termine: {} entree(s).", + removed + ) + } + "clearroles" => { + let removed = clear_role_permissions(&pool, bot_id).await.unwrap_or(0); + format!("Reset ACL roles termine: {} entree(s).", removed) + } + _ => "Action inconnue.".to_string(), + }; + + update_panel_message( + ctx, + component, + &pool, + bot_id, + MenuView::Overview, + Some(&status), + ) + .await; + return true; + } + + false +} + +pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) -> bool { + let Some((action, owner_id)) = parse_modal_custom_id(&modal.data.custom_id) else { + return false; + }; + + if modal.user.id.get() != owner_id { + modal_ephemeral( + ctx, + modal, + "Seul l'auteur de la commande peut utiliser ce panel.", + ) + .await; + return true; + } + + let bot_id = ctx.cache.current_user().id; + let Some(pool) = get_pool(ctx).await else { + modal_ephemeral(ctx, modal, "DB indisponible.").await; + return true; + }; + + let result = match action.as_str() { + "setcmd" => apply_set_command_modal(modal, &pool, bot_id).await, + "grant" => apply_grant_modal(modal, &pool, bot_id).await, + "del" => apply_delete_modal(modal, &pool, bot_id).await, + _ => Err("Action modal inconnue.".to_string()), + }; + + let response_text = match result { + Ok(message) => message, + Err(message) => message, + }; + + modal_ephemeral(ctx, modal, &response_text).await; + true +} + +pub async fn handle_permsmenu(ctx: &Context, msg: &Message, args: &[&str]) { + 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 snapshot = acl_snapshot(ctx, &pool, bot_id).await; + let color = theme_color(ctx).await; + let embed = build_overview_embed(&snapshot, color, None); + let components = build_components(msg.author.id, MenuView::Overview); + + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new().embed(embed).components(components), + ) + .await; +} + +pub struct PermsmenuCommand; +pub static COMMAND_DESCRIPTOR: PermsmenuCommand = PermsmenuCommand; + +impl crate::commands::command_contract::CommandSpec for PermsmenuCommand { + fn metadata(&self) -> crate::commands::command_contract::CommandMetadata { + crate::commands::command_contract::CommandMetadata { + name: "permsmenu", + category: "perms", + params: "aucun", + description: "Ouvre un panel ACL v2 avec embed et composants pour gerer le systeme de permissions.", + examples: &["+permsmenu", "+pmenu", "+help permsmenu"], + default_aliases: &["pmenu", "prmenu"], + allow_in_dm: false, + default_permission: 9, + } + } +} diff --git a/src/events/interaction_create.rs b/src/events/interaction_create.rs index f60981a..05103a1 100644 --- a/src/events/interaction_create.rs +++ b/src/events/interaction_create.rs @@ -3,7 +3,7 @@ use serenity::prelude::*; use crate::commands::{ advanced_tools, ancien, autoconfiglog, boostembed, g2048, help, helpsetting, morpion, mp, - perms_service, puissance4, rolemenu, suggestion, tempvoc, ticket, viewlogs, + perms_service, permsmenu, puissance4, rolemenu, suggestion, tempvoc, ticket, viewlogs, }; pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) { @@ -66,6 +66,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) return; } + if permsmenu::handle_component_interaction(ctx, component).await { + return; + } + if perms_service::handle_allperms_component(ctx, component).await { return; } @@ -99,6 +103,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) return; } + if permsmenu::handle_modal_interaction(ctx, modal).await { + return; + } + if rolemenu::handle_modal_interaction(ctx, modal).await { return; } diff --git a/src/events/message.rs b/src/events/message.rs index 3ca1ff5..1fc77db 100644 --- a/src/events/message.rs +++ b/src/events/message.rs @@ -18,9 +18,9 @@ use crate::commands::{ kiss, leave, leave_settings, link, listen, loading, lock, lockall, mainprefix, marry, massiverole, member, messagelog, modlog, morpion, mp, mpdelete, mpsent, mpsettings, mute, mutelist, muterole, newsticker, noderank, noderankadd, noderankdel, nolog, online, owner, - pendu, perms, pfc, pic, piconly, piconlyadd, piconlydel, ping, playto, prefix, public, - puissance4, punish, punishadd, punishdel, punishsetup, raidlog, removeinvite, rename, renew, - reroll, resetantiraide, rickroll, role, rolelog, rolemembers, rolemenu, sanctions, say, + pendu, perms, permsmenu, pfc, pic, piconly, piconlyadd, piconlydel, ping, playto, prefix, + public, puissance4, punish, punishadd, punishdel, punishsetup, raidlog, removeinvite, rename, + renew, reroll, resetantiraide, rickroll, role, rolelog, rolemembers, rolemenu, sanctions, say, serverbanner, serverinfo, serverlist, serverpic, set_boostembed, set_modlogs, set_muterole, setbanner, setname, setperm, setpic, setprofil, shadowbot, showpics, slot, slowmode, snake, snipe, spam, stream, strikes, suggestion, suggestionsettings, sync, tempban, tempcmute, @@ -470,6 +470,7 @@ pub async fn handle_message(ctx: &Context, msg: &Message) { "mainprefix" => mainprefix::handle_mainprefix(ctx, msg, &args).await, "prefix" => prefix::handle_prefix(ctx, msg, &args).await, "perms" => perms::handle_perms(ctx, msg, &args).await, + "permsmenu" => permsmenu::handle_permsmenu(ctx, msg, &args).await, "allperms" => allperms::handle_allperms(ctx, msg, &args).await, "clearowners" => clear_owners::handle_clear_owners(ctx, msg).await, "clearbl" => clear_bl::handle_clear_bl(ctx, msg).await,