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
+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,