Ajout du module permsmenu et intégration dans les commandes et interactions pour la gestion des permissions.

&& ajoute du readme
This commit is contained in:
Puechberty Arthur
2026-04-10 22:43:37 +02:00
parent 618e222759
commit 1f774628c2
7 changed files with 997 additions and 7 deletions
+25
View File
@@ -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 dune 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
Lusage 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é dune 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 daucune sorte. Lauteur décline toute responsabilité pour tout dommage direct ou indirect résultant de lutilisation du code.
+178
View File
@@ -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 <prefix>`
- Prefixe serveur modifiable: `+prefix <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`.
+3
View File
@@ -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<CommandMetadata> {
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(),
+3 -3
View File
@@ -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"),
+775
View File
@@ -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<Self> {
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<String>,
}
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::<u64>().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::<u64>().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<String> {
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<CreateActionRow> {
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<String, String> {
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::<u8>()
.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<String, String> {
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::<u8>().is_ok());
if is_level_mode {
let level = value
.parse::<u8>()
.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<String, String> {
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,
}
}
}
+9 -1
View File
@@ -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;
}
+4 -3
View File
@@ -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,