mirror of
https://github.com/arthur-pbty/shadowbot.git
synced 2026-06-05 00:06:23 +02:00
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:
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user