mirror of
https://github.com/arthur-pbty/shadowbot.git
synced 2026-06-03 23:36:25 +02:00
1026 lines
32 KiB
Rust
1026 lines
32 KiB
Rust
use std::collections::BTreeMap;
|
|
|
|
use serenity::builder::{
|
|
CreateActionRow, CreateButton, CreateCommand, CreateEmbed, CreateInteractionResponse,
|
|
CreateInteractionResponseMessage, CreateMessage, CreateSelectMenu, CreateSelectMenuKind,
|
|
CreateSelectMenuOption,
|
|
};
|
|
use serenity::model::application::{
|
|
Command, CommandInteraction, ComponentInteractionDataKind, Interaction,
|
|
};
|
|
use serenity::model::prelude::*;
|
|
use serenity::prelude::*;
|
|
|
|
use crate::commands::alias::resolve_alias;
|
|
use crate::commands::common::truncate_text;
|
|
use crate::db::{
|
|
DbPoolKey, get_help_aliases_enabled, get_help_perms_enabled, get_help_type,
|
|
list_command_aliases,
|
|
};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum HelpLayout {
|
|
Button,
|
|
Select,
|
|
Hybrid,
|
|
}
|
|
|
|
impl HelpLayout {
|
|
fn from_str(value: &str) -> Self {
|
|
match value.to_lowercase().as_str() {
|
|
"select" => Self::Select,
|
|
"hybrid" => Self::Hybrid,
|
|
_ => Self::Button,
|
|
}
|
|
}
|
|
|
|
fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Button => "button",
|
|
Self::Select => "select",
|
|
Self::Hybrid => "hybrid",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct HelpPage {
|
|
key: &'static str,
|
|
title: &'static str,
|
|
description: &'static str,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct CommandDoc {
|
|
key: &'static str,
|
|
command: &'static str,
|
|
allow_in_dm: bool,
|
|
default_permission: u8,
|
|
params: &'static str,
|
|
description: &'static str,
|
|
examples: &'static [&'static str],
|
|
alias_source_key: Option<&'static str>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct HelpViewPage {
|
|
category_index: Option<usize>,
|
|
category_title: &'static str,
|
|
category_description: &'static str,
|
|
section_index: usize,
|
|
section_total: usize,
|
|
commands_block: String,
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct HelpState {
|
|
layout: HelpLayout,
|
|
aliases_enabled: bool,
|
|
perms_enabled: bool,
|
|
}
|
|
|
|
const HELP_FALLBACK_EXAMPLES: &[&str] = &["+help", "+help ping"];
|
|
const HELP_PAGES: &[HelpPage] = &[
|
|
HelpPage {
|
|
key: "infos",
|
|
title: "Infos",
|
|
description: "Informations sur le serveur, les membres et les profils.",
|
|
},
|
|
HelpPage {
|
|
key: "logs",
|
|
title: "Logs",
|
|
description: "Configuration des logs de modération, messages, vocal et boosts.",
|
|
},
|
|
HelpPage {
|
|
key: "moderation",
|
|
title: "Modération",
|
|
description: "Sanctions, nettoyage et commandes de modération générale.",
|
|
},
|
|
HelpPage {
|
|
key: "roles",
|
|
title: "Rôles",
|
|
description: "Gestion des rôles individuels, temporaires et massifs.",
|
|
},
|
|
HelpPage {
|
|
key: "salons_vocal",
|
|
title: "Salons & Vocal",
|
|
description: "Gestion des salons texte et commandes de déplacement vocal.",
|
|
},
|
|
HelpPage {
|
|
key: "outils",
|
|
title: "Outils",
|
|
description: "Giveaways, utilitaires, embeds et automatisations de contenu.",
|
|
},
|
|
HelpPage {
|
|
key: "bot",
|
|
title: "Bot & Présence",
|
|
description: "Configuration du bot, thème, activité et présence.",
|
|
},
|
|
HelpPage {
|
|
key: "administration",
|
|
title: "Administration",
|
|
description: "Owners, blacklist, préfixes, MP et configuration globale.",
|
|
},
|
|
HelpPage {
|
|
key: "permissions",
|
|
title: "Permissions & Aide",
|
|
description: "Permissions, alias et configuration de l'interface d'aide.",
|
|
},
|
|
];
|
|
|
|
fn help_page_for_command(
|
|
meta: &crate::commands::command_contract::CommandMetadata,
|
|
) -> &'static str {
|
|
match meta.name {
|
|
"modlog" | "messagelog" | "voicelog" | "boostlog" | "rolelog" | "raidlog"
|
|
| "autoconfiglog" | "nolog" | "join" | "boostembed" | "set_modlogs" | "set_boostembed"
|
|
| "leave_settings" | "viewlogs" => "logs",
|
|
"warn"
|
|
| "mute"
|
|
| "tempmute"
|
|
| "unmute"
|
|
| "cmute"
|
|
| "tempcmute"
|
|
| "uncmute"
|
|
| "mutelist"
|
|
| "unmuteall"
|
|
| "kick"
|
|
| "ban"
|
|
| "tempban"
|
|
| "unban"
|
|
| "banlist"
|
|
| "unbanall"
|
|
| "sanctions"
|
|
| "del_sanction"
|
|
| "clear_sanctions"
|
|
| "clear_all_sanctions"
|
|
| "cleanup"
|
|
| "renew"
|
|
| "clear_messages" => "moderation",
|
|
"addrole" | "delrole" | "derank" | "massiverole" | "unmassiverole" | "temprole"
|
|
| "untemprole" | "sync" => "roles",
|
|
"lock" | "unlock" | "lockall" | "unlockall" | "hide" | "unhide" | "hideall"
|
|
| "unhideall" | "voicemove" | "voicekick" | "bringall" => "salons_vocal",
|
|
"giveaway" | "end" | "reroll" | "choose" | "calc" | "emoji" | "embed" | "say"
|
|
| "create" | "newsticker" | "button" | "autoreact" | "snipe" | "loading" | "backup"
|
|
| "autobackup" => "outils",
|
|
"shadowbot" | "set" | "theme" | "playto" | "listen" | "watch" | "compet" | "stream"
|
|
| "remove_activity" | "online" | "idle" | "dnd" | "invisible" | "change" | "changeall" => {
|
|
"bot"
|
|
}
|
|
"owner" | "unowner" | "clear_owners" | "bl" | "unbl" | "blinfo" | "clear_bl"
|
|
| "allbots" | "alladmins" | "botadmins" | "mainprefix" | "prefix" | "mp" | "invite"
|
|
| "leave" | "discussion" => "administration",
|
|
"perms" | "del" | "clear_perms" | "allperms" | "alias" | "help" | "helpsetting" => {
|
|
"permissions"
|
|
}
|
|
_ => match meta.category {
|
|
"infos" => "infos",
|
|
"logs" => "logs",
|
|
"moderation" => "moderation",
|
|
"roles" => "roles",
|
|
"salons_vocal" => "salons_vocal",
|
|
"outils" => "outils",
|
|
"bot" => "bot",
|
|
"administration" => "administration",
|
|
"permissions" => "permissions",
|
|
"general" => "infos",
|
|
"profile" => "bot",
|
|
"admin" => "administration",
|
|
_ => "infos",
|
|
},
|
|
}
|
|
}
|
|
|
|
fn help_page_title(key: &str) -> &'static str {
|
|
HELP_PAGES
|
|
.iter()
|
|
.find(|page| page.key == key)
|
|
.map(|page| page.title)
|
|
.unwrap_or("Infos")
|
|
}
|
|
|
|
fn help_page_title_for_command_key(key: &str) -> &'static str {
|
|
crate::commands::command_metadata_by_key(key)
|
|
.map(|meta| help_page_title(help_page_for_command(&meta)))
|
|
.unwrap_or("Infos")
|
|
}
|
|
|
|
fn help_metadata_lookup_key(input: &str) -> Option<&'static str> {
|
|
let normalized = help_lookup_key(input);
|
|
let underscored = normalized.replace(' ', "_");
|
|
|
|
crate::commands::all_command_metadata()
|
|
.into_iter()
|
|
.find(|meta| {
|
|
meta.name.eq_ignore_ascii_case(&normalized)
|
|
|| meta.name.eq_ignore_ascii_case(&underscored)
|
|
|| meta
|
|
.name
|
|
.replace('_', " ")
|
|
.eq_ignore_ascii_case(&normalized)
|
|
})
|
|
.map(|meta| meta.name)
|
|
}
|
|
|
|
fn help_page_matches_input(page: &HelpPage, input: &str) -> bool {
|
|
let normalized = help_lookup_key(input);
|
|
let aliases = match page.key {
|
|
"infos" => &["general", "info", "informations"][..],
|
|
"logs" => &["log", "journal"][..],
|
|
"moderation" => &["mod", "sanction"][..],
|
|
"roles" => &["role", "roles"][..],
|
|
"salons_vocal" => &["salon", "salons", "vocal", "voice", "channels"][..],
|
|
"outils" => &["utilitaires", "tools", "giveaway"][..],
|
|
"bot" => &["profil", "presence", "activite", "activity"][..],
|
|
"administration" => &["admin", "admins"][..],
|
|
"permissions" => &["permission", "perms", "aide", "help"][..],
|
|
_ => &[][..],
|
|
};
|
|
|
|
page.key.eq_ignore_ascii_case(&normalized)
|
|
|| help_lookup_key(page.title).eq_ignore_ascii_case(&normalized)
|
|
|| aliases
|
|
.iter()
|
|
.any(|alias| alias.eq_ignore_ascii_case(&normalized))
|
|
}
|
|
|
|
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
|
|
let data = ctx.data.read().await;
|
|
data.get::<DbPoolKey>().cloned()
|
|
}
|
|
|
|
async fn current_help_state(ctx: &Context) -> HelpState {
|
|
let bot_id = ctx.cache.current_user().id;
|
|
let pool = pool(ctx).await;
|
|
|
|
let layout = if let Some(pool) = &pool {
|
|
get_help_type(pool, bot_id)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.map(|value| HelpLayout::from_str(&value))
|
|
.unwrap_or(HelpLayout::Button)
|
|
} else {
|
|
HelpLayout::Button
|
|
};
|
|
|
|
let aliases_enabled = if let Some(pool) = &pool {
|
|
get_help_aliases_enabled(pool, bot_id)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(true)
|
|
} else {
|
|
true
|
|
};
|
|
|
|
let perms_enabled = if let Some(pool) = &pool {
|
|
get_help_perms_enabled(pool, bot_id)
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
.unwrap_or(true)
|
|
} else {
|
|
true
|
|
};
|
|
|
|
HelpState {
|
|
layout,
|
|
aliases_enabled,
|
|
perms_enabled,
|
|
}
|
|
}
|
|
|
|
async fn aliases_map(ctx: &Context) -> BTreeMap<String, Vec<String>> {
|
|
let bot_id = ctx.cache.current_user().id;
|
|
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
|
|
|
for meta in crate::commands::all_command_metadata() {
|
|
if !meta.default_aliases.is_empty() {
|
|
out.entry(meta.name.to_string())
|
|
.or_default()
|
|
.extend(meta.default_aliases.iter().map(|alias| alias.to_string()));
|
|
}
|
|
}
|
|
|
|
if let Some(pool) = pool(ctx).await {
|
|
if let Ok(rows) = list_command_aliases(&pool, bot_id).await {
|
|
for (alias, command) in rows {
|
|
out.entry(command).or_default().push(alias);
|
|
}
|
|
}
|
|
}
|
|
|
|
for aliases in out.values_mut() {
|
|
aliases.sort();
|
|
aliases.dedup();
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn command_doc(key: &str) -> Option<CommandDoc> {
|
|
let meta = match key {
|
|
"mp_settings" | "mp_sent" | "mp_delete" => crate::commands::command_metadata_by_key("mp")?,
|
|
"server_list" => crate::commands::command_metadata_by_key("server")?,
|
|
"change_reset" => crate::commands::command_metadata_by_key("change")?,
|
|
"set_perm" => crate::commands::command_metadata_by_key("set")?,
|
|
"del_perm" => crate::commands::command_metadata_by_key("del")?,
|
|
other => crate::commands::command_metadata_by_key(other)?,
|
|
};
|
|
|
|
Some(CommandDoc {
|
|
key: meta.name,
|
|
command: meta.name,
|
|
allow_in_dm: meta.allow_in_dm,
|
|
default_permission: meta.default_permission,
|
|
params: meta.params,
|
|
description: meta.description,
|
|
examples: if meta.examples.is_empty() {
|
|
HELP_FALLBACK_EXAMPLES
|
|
} else {
|
|
meta.examples
|
|
},
|
|
alias_source_key: Some(meta.name),
|
|
})
|
|
}
|
|
|
|
fn help_lookup_key(input: &str) -> String {
|
|
input
|
|
.trim()
|
|
.trim_start_matches('+')
|
|
.to_lowercase()
|
|
.replace('_', " ")
|
|
.split_whitespace()
|
|
.collect::<Vec<_>>()
|
|
.join(" ")
|
|
}
|
|
|
|
fn help_lookup_to_key(input: &str) -> Option<&'static str> {
|
|
let matched = match help_lookup_key(input).as_str() {
|
|
"help" => Some("help"),
|
|
"ping" => Some("ping"),
|
|
"allbots" => Some("allbots"),
|
|
"alladmins" => Some("alladmins"),
|
|
"botadmins" => Some("botadmins"),
|
|
"boosters" => Some("boosters"),
|
|
"rolemembers" => Some("rolemembers"),
|
|
"serverinfo" => Some("serverinfo"),
|
|
"vocinfo" => Some("vocinfo"),
|
|
"role" => Some("role"),
|
|
"channel" => Some("channel"),
|
|
"user" => Some("user"),
|
|
"member" => Some("member"),
|
|
"pic" => Some("pic"),
|
|
"banner" => Some("banner"),
|
|
"server" => Some("server"),
|
|
"server list" => Some("server_list"),
|
|
"snipe" => Some("snipe"),
|
|
"emoji" => Some("emoji"),
|
|
"giveaway" => Some("giveaway"),
|
|
"end" | "end giveaway" => Some("end"),
|
|
"reroll" => Some("reroll"),
|
|
"choose" => Some("choose"),
|
|
"embed" => Some("embed"),
|
|
"backup" | "backup list" | "backup delete" | "backup load" => Some("backup"),
|
|
"autobackup" => Some("autobackup"),
|
|
"loading" => Some("loading"),
|
|
"create" => Some("create"),
|
|
"newsticker" => Some("newsticker"),
|
|
"massiverole" => Some("massiverole"),
|
|
"unmassiverole" => Some("unmassiverole"),
|
|
"voicemove" => Some("voicemove"),
|
|
"voicekick" => Some("voicekick"),
|
|
"cleanup" => Some("cleanup"),
|
|
"bringall" => Some("bringall"),
|
|
"renew" => Some("renew"),
|
|
"unbanall" => Some("unbanall"),
|
|
"temprole" => Some("temprole"),
|
|
"untemprole" => Some("untemprole"),
|
|
"sync" => Some("sync"),
|
|
"button" => Some("button"),
|
|
"autoreact" => Some("autoreact"),
|
|
"calc" => Some("calc"),
|
|
"shadowbot" => Some("shadowbot"),
|
|
"set" => Some("set"),
|
|
"theme" => Some("theme"),
|
|
"playto" => Some("playto"),
|
|
"listen" => Some("listen"),
|
|
"watch" => Some("watch"),
|
|
"compet" => Some("compet"),
|
|
"stream" => Some("stream"),
|
|
"remove activity" => Some("remove_activity"),
|
|
"online" => Some("online"),
|
|
"idle" => Some("idle"),
|
|
"dnd" => Some("dnd"),
|
|
"invisible" => Some("invisible"),
|
|
"mp" => Some("mp"),
|
|
"mp settings" => Some("mp_settings"),
|
|
"mp sent" => Some("mp_sent"),
|
|
"mp delete" | "mp del" => Some("mp_delete"),
|
|
"discussion" => Some("discussion"),
|
|
"owner" => Some("owner"),
|
|
"unowner" => Some("unowner"),
|
|
"clear owners" => Some("clear_owners"),
|
|
"bl" => Some("bl"),
|
|
"unbl" => Some("unbl"),
|
|
"blinfo" => Some("blinfo"),
|
|
"clear bl" => Some("clear_bl"),
|
|
"say" => Some("say"),
|
|
"invite" => Some("invite"),
|
|
"leave" => Some("leave"),
|
|
"change" => Some("change"),
|
|
"change reset" => Some("change_reset"),
|
|
"changeall" => Some("changeall"),
|
|
"mainprefix" => Some("mainprefix"),
|
|
"prefix" => Some("prefix"),
|
|
"perms" => Some("perms"),
|
|
"allperms" => Some("allperms"),
|
|
"set perm" => Some("set_perm"),
|
|
"del perm" => Some("del_perm"),
|
|
"clear perms" => Some("clear_perms"),
|
|
"alias" => Some("alias"),
|
|
"helpsetting" | "helpetting" => Some("helpsetting"),
|
|
_ => None,
|
|
};
|
|
|
|
matched.or_else(|| help_metadata_lookup_key(input))
|
|
}
|
|
|
|
fn help_page_index(key: &str) -> Option<usize> {
|
|
HELP_PAGES
|
|
.iter()
|
|
.position(|page| help_page_matches_input(page, key))
|
|
}
|
|
|
|
fn help_page_from_input(input: &str) -> Option<usize> {
|
|
if let Ok(index) = input.parse::<usize>() {
|
|
if index >= 1 && index <= HELP_PAGES.len() {
|
|
return Some(index - 1);
|
|
}
|
|
}
|
|
|
|
help_page_index(input)
|
|
}
|
|
|
|
fn format_permission_level(level: u8) -> String {
|
|
format!("[{}]", level)
|
|
}
|
|
|
|
fn permission_level_description(level: u8) -> &'static str {
|
|
match level {
|
|
0 => "[0] Public",
|
|
2 => "[2] Accès spécial",
|
|
8 => "[8] Modérateur+",
|
|
9 => "[9] Propriétaire",
|
|
_ => "[?] Inconnu",
|
|
}
|
|
}
|
|
|
|
fn help_page_content(
|
|
page: &HelpPage,
|
|
alias_map: &BTreeMap<String, Vec<String>>,
|
|
aliases_enabled: bool,
|
|
perms_enabled: bool,
|
|
) -> Vec<String> {
|
|
let mut commands = crate::commands::all_command_metadata()
|
|
.into_iter()
|
|
.filter(|meta| help_page_for_command(meta).eq_ignore_ascii_case(page.key))
|
|
.collect::<Vec<_>>();
|
|
commands.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
|
|
|
let mut lines = Vec::with_capacity(commands.len());
|
|
|
|
for meta in commands {
|
|
let label = meta.name.replace('_', " ");
|
|
let alias_key = meta.name;
|
|
let params = if meta.params.trim().is_empty() {
|
|
"aucun"
|
|
} else {
|
|
meta.params
|
|
};
|
|
let permission = if perms_enabled {
|
|
format_permission_level(meta.default_permission)
|
|
} else {
|
|
"désactivée".to_string()
|
|
};
|
|
let aliases_text = if aliases_enabled {
|
|
alias_map
|
|
.get(alias_key)
|
|
.filter(|aliases| !aliases.is_empty())
|
|
.map(|aliases| {
|
|
aliases
|
|
.iter()
|
|
.map(|alias| format!("`{}`", alias))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
})
|
|
.unwrap_or_else(|| "aucun".to_string())
|
|
} else {
|
|
"désactivés".to_string()
|
|
};
|
|
|
|
let first_line = format!(
|
|
"`+{}` · args: `{}` · perm: `{}` · aliases: {}",
|
|
label, params, permission, aliases_text
|
|
);
|
|
let second_line = meta.description;
|
|
|
|
lines.push(format!("{}\n{}", first_line, second_line));
|
|
}
|
|
|
|
if lines.is_empty() {
|
|
lines.push("Aucune commande dans cette catégorie.".to_string());
|
|
}
|
|
|
|
lines
|
|
}
|
|
|
|
fn paginate_blocks(blocks: &[String], max_chars: usize) -> Vec<String> {
|
|
let mut pages = Vec::new();
|
|
let mut current = String::new();
|
|
|
|
for block in blocks {
|
|
let block_len = block.chars().count();
|
|
|
|
if current.is_empty() {
|
|
if block_len > max_chars {
|
|
pages.push(truncate_text(block, max_chars));
|
|
} else {
|
|
current = block.clone();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
let separator = "\n\n";
|
|
let next_len = current.chars().count() + separator.chars().count() + block_len;
|
|
if next_len > max_chars {
|
|
pages.push(current);
|
|
if block_len > max_chars {
|
|
pages.push(truncate_text(block, max_chars));
|
|
current = String::new();
|
|
} else {
|
|
current = block.clone();
|
|
}
|
|
} else {
|
|
current.push_str(separator);
|
|
current.push_str(block);
|
|
}
|
|
}
|
|
|
|
if !current.is_empty() {
|
|
pages.push(current);
|
|
}
|
|
|
|
if pages.is_empty() {
|
|
pages.push("Aucune commande dans cette catégorie.".to_string());
|
|
}
|
|
|
|
pages
|
|
}
|
|
|
|
fn help_commands_per_category() -> Vec<usize> {
|
|
let mut counts = vec![0usize; HELP_PAGES.len()];
|
|
|
|
for meta in crate::commands::all_command_metadata() {
|
|
let key = help_page_for_command(&meta);
|
|
if let Some(index) = HELP_PAGES.iter().position(|page| page.key == key) {
|
|
counts[index] += 1;
|
|
}
|
|
}
|
|
|
|
counts
|
|
}
|
|
|
|
fn build_help_view_pages(
|
|
state: &HelpState,
|
|
alias_map: &BTreeMap<String, Vec<String>>,
|
|
) -> Vec<HelpViewPage> {
|
|
const MAX_COMMANDS_BLOCK_CHARS: usize = 3600;
|
|
|
|
let mut out = Vec::new();
|
|
|
|
let counts = help_commands_per_category();
|
|
let total_commands: usize = counts.iter().sum();
|
|
let mut intro_lines = Vec::with_capacity(4 + HELP_PAGES.len());
|
|
intro_lines.push("Shadow Bot est un bot de gestion de serveur.".to_string());
|
|
intro_lines.push(String::new());
|
|
intro_lines.push(format!("**Nombre total de commandes :** {}", total_commands));
|
|
intro_lines.push("**Nombre de commandes par catégorie :**".to_string());
|
|
for (index, page) in HELP_PAGES.iter().enumerate() {
|
|
intro_lines.push(format!("• {} : {}", page.title, counts[index]));
|
|
}
|
|
|
|
out.push(HelpViewPage {
|
|
category_index: None,
|
|
category_title: "Présentation",
|
|
category_description: "Présentation de Shadow Bot et statistiques globales.",
|
|
section_index: 0,
|
|
section_total: 1,
|
|
commands_block: intro_lines.join("\n"),
|
|
});
|
|
|
|
for (category_index, page) in HELP_PAGES.iter().enumerate() {
|
|
let blocks = help_page_content(page, alias_map, state.aliases_enabled, state.perms_enabled);
|
|
let sections = paginate_blocks(&blocks, MAX_COMMANDS_BLOCK_CHARS);
|
|
let section_total = sections.len();
|
|
|
|
for (section_index, commands_block) in sections.into_iter().enumerate() {
|
|
out.push(HelpViewPage {
|
|
category_index: Some(category_index),
|
|
category_title: page.title,
|
|
category_description: page.description,
|
|
section_index,
|
|
section_total,
|
|
commands_block,
|
|
});
|
|
}
|
|
}
|
|
|
|
out
|
|
}
|
|
|
|
fn first_view_page_for_category(view_pages: &[HelpViewPage], category_index: usize) -> usize {
|
|
view_pages
|
|
.iter()
|
|
.position(|entry| entry.category_index == Some(category_index))
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn bot_avatar_url(ctx: &Context) -> String {
|
|
ctx.cache.current_user().face()
|
|
}
|
|
|
|
fn build_help_embed(
|
|
page_index: usize,
|
|
state: &HelpState,
|
|
view_pages: &[HelpViewPage],
|
|
avatar_url: &str,
|
|
) -> CreateEmbed {
|
|
let safe_page_index = page_index.min(view_pages.len().saturating_sub(1));
|
|
let view = &view_pages[safe_page_index];
|
|
let is_intro = view.category_index.is_none();
|
|
|
|
let title = if view.section_total > 1 {
|
|
format!(
|
|
"Aide · {} {}/{}",
|
|
view.category_title,
|
|
view.section_index + 1,
|
|
view.section_total
|
|
)
|
|
} else {
|
|
format!("Aide · {}", view.category_title)
|
|
};
|
|
|
|
let header = format!(
|
|
"Page {}/{} · mode `{}` · aliases {} · perms {}\n{}",
|
|
safe_page_index + 1,
|
|
view_pages.len(),
|
|
state.layout.as_str(),
|
|
if state.aliases_enabled {
|
|
"activés"
|
|
} else {
|
|
"désactivés"
|
|
},
|
|
if state.perms_enabled {
|
|
"activées"
|
|
} else {
|
|
"désactivées"
|
|
},
|
|
view.category_description,
|
|
);
|
|
|
|
if is_intro {
|
|
return CreateEmbed::new()
|
|
.title(title)
|
|
.description(format!("{}\n\n{}", header, view.commands_block))
|
|
.thumbnail(avatar_url)
|
|
.color(0x5865F2);
|
|
}
|
|
|
|
let commands_intro = "\n\n**Commandes**\n";
|
|
let available = 4096usize
|
|
.saturating_sub(header.chars().count())
|
|
.saturating_sub(commands_intro.chars().count());
|
|
let commands_block = truncate_text(&view.commands_block, available);
|
|
|
|
CreateEmbed::new()
|
|
.title(title)
|
|
.description(format!("{}{}{}", header, commands_intro, commands_block))
|
|
.color(0x5865F2)
|
|
}
|
|
|
|
fn help_components(
|
|
owner_id: UserId,
|
|
page_index: usize,
|
|
state: &HelpState,
|
|
view_pages: &[HelpViewPage],
|
|
) -> Vec<CreateActionRow> {
|
|
let total = view_pages.len().max(1);
|
|
let prev_page = page_index.saturating_sub(1);
|
|
let next_page = (page_index + 1).min(total - 1);
|
|
let custom_prev = format!("help:nav:{}:{}", owner_id.get(), prev_page);
|
|
let custom_next = format!("help:nav:{}:{}", owner_id.get(), next_page);
|
|
|
|
let mut rows = Vec::new();
|
|
|
|
match state.layout {
|
|
HelpLayout::Button | HelpLayout::Hybrid => {
|
|
rows.push(CreateActionRow::Buttons(vec![
|
|
CreateButton::new(custom_prev)
|
|
.label("◀ Précédent")
|
|
.style(ButtonStyle::Primary)
|
|
.disabled(page_index == 0),
|
|
CreateButton::new(custom_next)
|
|
.label("Suivant ▶")
|
|
.style(ButtonStyle::Primary)
|
|
.disabled(page_index + 1 >= total),
|
|
]));
|
|
}
|
|
HelpLayout::Select => {}
|
|
}
|
|
|
|
match state.layout {
|
|
HelpLayout::Select | HelpLayout::Hybrid => {
|
|
let mut options = Vec::with_capacity(HELP_PAGES.len() + 1);
|
|
options.push(
|
|
CreateSelectMenuOption::new("Présentation", "0").description(
|
|
"Shadow Bot, total des commandes et répartition par catégorie.",
|
|
),
|
|
);
|
|
|
|
for (index, page) in HELP_PAGES.iter().enumerate() {
|
|
let count = view_pages
|
|
.iter()
|
|
.filter(|entry| entry.category_index == Some(index))
|
|
.count()
|
|
.max(1);
|
|
let first_page = first_view_page_for_category(view_pages, index);
|
|
let title = if count > 1 {
|
|
format!("{} ({})", page.title, count)
|
|
} else {
|
|
page.title.to_string()
|
|
};
|
|
|
|
options.push(
|
|
CreateSelectMenuOption::new(title, first_page.to_string())
|
|
.description(truncate_text(page.description, 100)),
|
|
);
|
|
}
|
|
|
|
let menu = CreateSelectMenu::new(
|
|
format!("help:select:{}", owner_id.get()),
|
|
CreateSelectMenuKind::String { options },
|
|
)
|
|
.placeholder("Choisir une page d'aide");
|
|
|
|
rows.push(CreateActionRow::SelectMenu(menu));
|
|
}
|
|
HelpLayout::Button => {}
|
|
}
|
|
|
|
rows
|
|
}
|
|
|
|
fn parse_help_component_id(custom_id: &str) -> Option<(&str, u64, Option<usize>)> {
|
|
let parts = custom_id.split(':').collect::<Vec<_>>();
|
|
if parts.len() < 3 || parts.first().copied()? != "help" {
|
|
return None;
|
|
}
|
|
|
|
let kind = parts.get(1).copied()?;
|
|
let owner_id = parts.get(2)?.parse::<u64>().ok()?;
|
|
let page = parts.get(3).and_then(|value| value.parse::<usize>().ok());
|
|
Some((kind, owner_id, page))
|
|
}
|
|
|
|
pub async fn register_slash_help(ctx: &Context) {
|
|
let _ = Command::create_global_command(
|
|
&ctx.http,
|
|
CreateCommand::new("help").description("Affiche l'aide du bot"),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub async fn handle_help_slash(ctx: &Context, command: &CommandInteraction) {
|
|
let state = current_help_state(ctx).await;
|
|
let alias_map = aliases_map(ctx).await;
|
|
let view_pages = build_help_view_pages(&state, &alias_map);
|
|
let avatar_url = bot_avatar_url(ctx);
|
|
let embed = build_help_embed(0, &state, &view_pages, &avatar_url);
|
|
let components = help_components(command.user.id, 0, &state, &view_pages);
|
|
|
|
let _ = command
|
|
.create_response(
|
|
&ctx.http,
|
|
CreateInteractionResponse::Message(
|
|
CreateInteractionResponseMessage::new()
|
|
.embed(embed)
|
|
.components(components),
|
|
),
|
|
)
|
|
.await;
|
|
}
|
|
|
|
pub async fn handle_slash_interaction(ctx: &Context, interaction: &Interaction) -> bool {
|
|
let Interaction::Command(command) = interaction else {
|
|
return false;
|
|
};
|
|
|
|
if command.data.name != "help" {
|
|
return false;
|
|
}
|
|
|
|
handle_help_slash(ctx, command).await;
|
|
true
|
|
}
|
|
|
|
pub async fn handle_help_component(ctx: &Context, component: &ComponentInteraction) -> bool {
|
|
let Some((kind, owner_id, page)) = parse_help_component_id(&component.data.custom_id) else {
|
|
return false;
|
|
};
|
|
|
|
if component.user.id.get() != owner_id {
|
|
let _ = component
|
|
.create_response(
|
|
&ctx.http,
|
|
CreateInteractionResponse::Message(
|
|
CreateInteractionResponseMessage::new()
|
|
.content("Seul l'auteur de l'aide peut utiliser ces contrôles.")
|
|
.ephemeral(true),
|
|
),
|
|
)
|
|
.await;
|
|
return true;
|
|
}
|
|
|
|
let state = current_help_state(ctx).await;
|
|
let alias_map = aliases_map(ctx).await;
|
|
let view_pages = build_help_view_pages(&state, &alias_map);
|
|
let avatar_url = bot_avatar_url(ctx);
|
|
let page_index = match kind {
|
|
"nav" => page.unwrap_or(0).min(view_pages.len().saturating_sub(1)),
|
|
"select" => match &component.data.kind {
|
|
ComponentInteractionDataKind::StringSelect { values } => values
|
|
.first()
|
|
.and_then(|value| value.parse::<usize>().ok())
|
|
.unwrap_or(0)
|
|
.min(view_pages.len().saturating_sub(1)),
|
|
_ => 0,
|
|
},
|
|
_ => 0,
|
|
};
|
|
|
|
let embed = build_help_embed(page_index, &state, &view_pages, &avatar_url);
|
|
let components = help_components(component.user.id, page_index, &state, &view_pages);
|
|
|
|
let _ = component
|
|
.create_response(
|
|
&ctx.http,
|
|
CreateInteractionResponse::UpdateMessage(
|
|
CreateInteractionResponseMessage::new()
|
|
.embed(embed)
|
|
.components(components),
|
|
),
|
|
)
|
|
.await;
|
|
|
|
true
|
|
}
|
|
|
|
pub async fn handle_help(ctx: &Context, msg: &Message, args: &[&str]) {
|
|
let state = current_help_state(ctx).await;
|
|
let alias_map = aliases_map(ctx).await;
|
|
|
|
if !args.is_empty() {
|
|
let joined = args.join(" ");
|
|
|
|
let mut resolved_key = help_lookup_to_key(&joined).map(|s| s.to_string());
|
|
if resolved_key.is_none() {
|
|
let first = args[0];
|
|
if let Some(key) = help_lookup_to_key(first) {
|
|
resolved_key = Some(key.to_string());
|
|
}
|
|
}
|
|
|
|
if resolved_key.is_none() {
|
|
let alias_input = help_lookup_key(&joined).replace(' ', "_");
|
|
if let Some(alias_target) = resolve_alias(ctx, &alias_input).await {
|
|
resolved_key = Some(alias_target);
|
|
}
|
|
}
|
|
|
|
if resolved_key.is_none() {
|
|
resolved_key = help_metadata_lookup_key(&joined).map(|key| key.to_string());
|
|
}
|
|
|
|
if let Some(key) = resolved_key {
|
|
if let Some(doc) = command_doc(&key) {
|
|
let aliases = doc
|
|
.alias_source_key
|
|
.and_then(|alias_key| alias_map.get(alias_key))
|
|
.cloned()
|
|
.unwrap_or_default();
|
|
|
|
let alias_text = if aliases.is_empty() {
|
|
"Aucun alias".to_string()
|
|
} else {
|
|
aliases
|
|
.iter()
|
|
.map(|alias| format!("`{}`", alias))
|
|
.collect::<Vec<_>>()
|
|
.join(", ")
|
|
};
|
|
|
|
let examples = doc
|
|
.examples
|
|
.iter()
|
|
.map(|ex| format!("`{}`", ex))
|
|
.collect::<Vec<_>>()
|
|
.join("\n");
|
|
|
|
let mut embed = CreateEmbed::new()
|
|
.title(format!(
|
|
"Aide commande · +{}",
|
|
doc.command.replace('_', " ")
|
|
))
|
|
.description(doc.description)
|
|
.field(
|
|
"Commande",
|
|
format!("`+{}`", doc.command.replace('_', " ")),
|
|
false,
|
|
)
|
|
.field("Clé ACL", format!("`{}`", doc.key), false)
|
|
.field("Catégorie", help_page_title_for_command_key(doc.key), false)
|
|
.field("Alias", alias_text, false)
|
|
.field("Paramètres", doc.params, false)
|
|
.field(
|
|
"Disponible en DM",
|
|
if doc.allow_in_dm { "Oui" } else { "Non" },
|
|
true,
|
|
)
|
|
.field("Exemples", truncate_text(&examples, 1024), false);
|
|
|
|
if state.perms_enabled {
|
|
embed = embed.field(
|
|
"Permission",
|
|
permission_level_description(doc.default_permission),
|
|
false,
|
|
);
|
|
}
|
|
|
|
embed = embed.color(crate::commands::common::theme_color(ctx).await);
|
|
|
|
let _ = msg
|
|
.channel_id
|
|
.send_message(&ctx.http, CreateMessage::new().embed(embed))
|
|
.await;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let category_index = args
|
|
.first()
|
|
.and_then(|input| help_page_from_input(input))
|
|
.unwrap_or(0)
|
|
.min(HELP_PAGES.len().saturating_sub(1));
|
|
|
|
let view_pages = build_help_view_pages(&state, &alias_map);
|
|
let avatar_url = bot_avatar_url(ctx);
|
|
let page_index = if args.is_empty() {
|
|
0
|
|
} else {
|
|
first_view_page_for_category(&view_pages, category_index)
|
|
};
|
|
|
|
let embed = build_help_embed(page_index, &state, &view_pages, &avatar_url);
|
|
let components = help_components(msg.author.id, page_index, &state, &view_pages);
|
|
|
|
let _ = msg
|
|
.channel_id
|
|
.send_message(
|
|
&ctx.http,
|
|
CreateMessage::new().embed(embed).components(components),
|
|
)
|
|
.await;
|
|
}
|
|
pub struct HelpCommand;
|
|
pub static COMMAND_DESCRIPTOR: HelpCommand = HelpCommand;
|
|
|
|
impl crate::commands::command_contract::CommandSpec for HelpCommand {
|
|
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
|
|
crate::commands::command_contract::CommandMetadata {
|
|
name: "help",
|
|
category: "permissions",
|
|
params: "[commande|page]",
|
|
description: "Affiche les pages daide du bot ou la fiche detaillee dune commande avec parametres, aliases et exemples.",
|
|
examples: &["+help", "+hp", "+help help"],
|
|
default_aliases: &["hp"],
|
|
allow_in_dm: true,
|
|
default_permission: 0,
|
|
}
|
|
}
|
|
}
|