feat(events): add message update, role, and voice state update handlers

- Implemented `handle_message_update` to log message edits.
- Created role event handlers for role creation, update, and deletion.
- Added voice state update handling to log channel changes.
- Introduced a new `ready_event` handler to restore bot presence and enforce blacklist.
- Updated `mod.rs` to include new event modules.
- Enhanced `main.rs` for database connection and initialization.
- Added comprehensive permission management in `permissions.rs`.
This commit is contained in:
Puechberty Arthur
2026-04-10 02:13:04 +02:00
commit 3e69185296
169 changed files with 23909 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
target
.git
.gitignore
.env
.env.*
+11
View File
@@ -0,0 +1,11 @@
# 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
+4
View File
@@ -0,0 +1,4 @@
/target
.env
.env.*
!.env.example
Generated
+3333
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "shadowbot"
version = "0.1.0"
edition = "2024"
[dependencies]
serenity = { version = "0.12", features = ["client", "gateway", "rustls_backend", "model"] }
tokio = { version = "1", features = ["full"] }
dotenv = "0.15"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "chrono"] }
chrono = { version = "0.4", features = ["clock"] }
meval = "0.2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
base64 = "0.22"
+23
View File
@@ -0,0 +1,23 @@
# syntax=docker/dockerfile:1.7
FROM rust:1-slim-bookworm AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
FROM debian:bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --create-home --uid 10001 appuser
COPY --from=builder /app/target/release/shadowbot /usr/local/bin/shadowbot
USER appuser
WORKDIR /home/appuser
ENV RUST_LOG=info
CMD ["shadowbot"]
+36
View File
@@ -0,0 +1,36 @@
services:
bot:
build:
context: .
dockerfile: Dockerfile
image: shadowbot:latest
container_name: shadowbot-bot
restart: unless-stopped
environment:
BOT_TOKEN: ${BOT_TOKEN}
DATABASE_URL: postgres://${POSTGRES_USER:-shadowbot}:${POSTGRES_PASSWORD:-shadowbot_dev_password}@postgres:5432/${POSTGRES_DB:-shadowbot}
depends_on:
postgres:
condition: service_healthy
postgres:
image: postgres:16-alpine
container_name: shadowbot-postgres
restart: unless-stopped
environment:
POSTGRES_DB: ${POSTGRES_DB:-shadowbot}
POSTGRES_USER: ${POSTGRES_USER:-shadowbot}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-shadowbot_dev_password}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-shadowbot} -d ${POSTGRES_DB:-shadowbot}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
volumes:
postgres_data:
+133
View File
@@ -0,0 +1,133 @@
use serenity::gateway::ActivityData;
use serenity::model::prelude::OnlineStatus;
use serenity::prelude::{Context, TypeMapKey};
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::{Duration, sleep};
pub struct ActivityTaskKey;
impl TypeMapKey for ActivityTaskKey {
type Value = Arc<Mutex<Option<JoinHandle<()>>>>;
}
#[derive(Clone, Copy, Debug)]
pub enum RotatingActivityKind {
Playing,
Listening,
Watching,
Competing,
Streaming,
}
impl RotatingActivityKind {
pub fn from_command(command: &str) -> Option<Self> {
match command {
"+playto" => Some(Self::Playing),
"+listen" => Some(Self::Listening),
"+watch" => Some(Self::Watching),
"+compet" => Some(Self::Competing),
"+stream" => Some(Self::Streaming),
_ => None,
}
}
pub fn from_db(value: &str) -> Option<Self> {
match value {
"playing" => Some(Self::Playing),
"listening" => Some(Self::Listening),
"watching" => Some(Self::Watching),
"competing" => Some(Self::Competing),
"streaming" => Some(Self::Streaming),
_ => None,
}
}
pub fn as_db(&self) -> &'static str {
match self {
Self::Playing => "playing",
Self::Listening => "listening",
Self::Watching => "watching",
Self::Competing => "competing",
Self::Streaming => "streaming",
}
}
fn to_activity(self, message: &str) -> ActivityData {
match self {
Self::Playing => ActivityData::playing(message),
Self::Listening => ActivityData::listening(message),
Self::Watching => ActivityData::watching(message),
Self::Competing => ActivityData::competing(message),
Self::Streaming => ActivityData::streaming(message, "https://twitch.tv/discord")
.unwrap_or_else(|_| ActivityData::playing(message)),
}
}
}
pub fn parse_status(value: &str) -> OnlineStatus {
match value {
"idle" => OnlineStatus::Idle,
"dnd" => OnlineStatus::DoNotDisturb,
"invisible" => OnlineStatus::Invisible,
_ => OnlineStatus::Online,
}
}
pub async fn stop_rotation(ctx: &Context) {
let task_slot = {
let mut data = ctx.data.write().await;
if !data.contains_key::<ActivityTaskKey>() {
data.insert::<ActivityTaskKey>(Arc::new(Mutex::new(None)));
}
data.get::<ActivityTaskKey>().cloned()
};
if let Some(slot) = task_slot {
let mut guard = slot.lock().await;
if let Some(handle) = guard.take() {
handle.abort();
}
}
}
pub async fn start_rotation(
ctx: &Context,
kind: RotatingActivityKind,
messages: Vec<String>,
status: OnlineStatus,
) {
if messages.is_empty() {
return;
}
stop_rotation(ctx).await;
let task_slot = {
let mut data = ctx.data.write().await;
if !data.contains_key::<ActivityTaskKey>() {
data.insert::<ActivityTaskKey>(Arc::new(Mutex::new(None)));
}
data.get::<ActivityTaskKey>().cloned()
};
let Some(slot) = task_slot else {
return;
};
let cloned_ctx = ctx.clone();
let handle = tokio::spawn(async move {
let mut index = 0usize;
loop {
let msg = &messages[index % messages.len()];
let activity = kind.to_activity(msg);
cloned_ctx.set_presence(Some(activity), status);
index = (index + 1) % messages.len();
sleep(Duration::from_secs(30)).await;
}
});
let mut guard = slot.lock().await;
*guard = Some(handle);
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_addrole(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_add_del_role(ctx, msg, args, true).await;
}
pub struct AddroleCommand;
pub static COMMAND_DESCRIPTOR: AddroleCommand = AddroleCommand;
impl crate::commands::command_contract::CommandSpec for AddroleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "addrole",
command: "addrole",
category: "admin",
params: "<@membre/ID[,..]> <@role/ID>",
summary: "Ajoute un role",
description: "Ajoute un role a un ou plusieurs membres.",
examples: &["+addrole @User @Membre"],
alias_source_key: "addrole",
default_aliases: &["ar"],
}
}
}
+53
View File
@@ -0,0 +1,53 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::permissions::is_owner_user;
pub fn parse_user_id(input: &str) -> Option<UserId> {
let cleaned = input
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!');
cleaned.parse::<u64>().ok().map(UserId::new)
}
pub async fn app_owner_id(ctx: &Context) -> Option<UserId> {
let info = ctx.http.get_current_application_info().await.ok()?;
info.owner.map(|u| u.id)
}
pub async fn ensure_owner(ctx: &Context, msg: &Message) -> Result<(), ()> {
if is_owner_user(ctx, msg.author.id).await {
Ok(())
} else {
let embed = CreateEmbed::new()
.title("Accès refusé")
.description("Cette commande est réservée aux owners du bot.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
Err(())
}
}
pub async fn ban_user_everywhere(ctx: &Context, user_id: UserId, reason: &str) -> (usize, usize) {
let guilds = ctx.cache.guilds();
let mut ok = 0usize;
let mut ko = 0usize;
for guild_id in guilds {
match guild_id
.ban_with_reason(&ctx.http, user_id, 0, reason)
.await
{
Ok(_) => ok += 1,
Err(_) => ko += 1,
}
}
(ok, ko)
}
+56
View File
@@ -0,0 +1,56 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::db::{DbPoolKey, is_blacklisted, list_blacklisted_ids};
pub async fn enforce_blacklist_on_message(ctx: &Context, msg: &Message) -> bool {
if msg.author.bot {
return false;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
return false;
};
let blacklisted = is_blacklisted(&pool, bot_id, msg.author.id)
.await
.unwrap_or(false);
if !blacklisted {
return false;
}
if let Some(guild_id) = msg.guild_id {
let _ = guild_id
.ban_with_reason(&ctx.http, msg.author.id, 0, "Blacklist globale du bot")
.await;
}
true
}
pub async fn enforce_blacklist_on_guild(ctx: &Context, guild_id: GuildId) {
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
return;
};
let users = list_blacklisted_ids(&pool, bot_id)
.await
.unwrap_or_default();
for uid in users {
let _ = guild_id
.ban_with_reason(&ctx.http, uid, 0, "Blacklist globale du bot")
.await;
}
}
File diff suppressed because it is too large Load Diff
+137
View File
@@ -0,0 +1,137 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{add_list_fields, send_embed};
use crate::db::{
DbPoolKey, get_command_alias, list_command_aliases, remove_command_alias, set_command_alias,
};
use crate::permissions::all_command_keys;
pub async fn handle_alias(ctx: &Context, msg: &Message, args: &[&str]) {
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.len() == 1 {
let aliases = list_command_aliases(&pool, bot_id)
.await
.unwrap_or_default();
let lines = aliases
.into_iter()
.map(|(alias, command)| format!("`{}` -> `{}`", alias, command))
.collect::<Vec<_>>();
let mut embed = serenity::builder::CreateEmbed::new()
.title("Aliases")
.color(0x5865F2);
embed = add_list_fields(embed, &lines, "Aliases enregistrés");
send_embed(ctx, msg, embed).await;
return;
}
if args.len() < 2 {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+alias <commande> <alias>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
if args[0].eq_ignore_ascii_case("remove") || args[0].eq_ignore_ascii_case("delete") {
let alias_name = args[1].trim_start_matches('+').to_lowercase();
if alias_name.is_empty() {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Alias invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let removed = remove_command_alias(&pool, bot_id, &alias_name)
.await
.unwrap_or(0);
let embed = serenity::builder::CreateEmbed::new()
.title("Alias supprimé")
.description(format!("`{}` : {} suppression(s).", alias_name, removed))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
return;
}
let command = args[0].trim_start_matches('+').to_lowercase();
if !all_command_keys()
.iter()
.any(|candidate| candidate == &command)
{
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Commande cible inconnue.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let alias_name = args[1].trim_start_matches('+').to_lowercase();
if alias_name.is_empty() {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Alias invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let _ = set_command_alias(&pool, bot_id, &alias_name, &command).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Alias créé")
.description(format!(
"`{}` devient un alias de `{}`",
alias_name, command
))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub async fn resolve_alias(ctx: &Context, command: &str) -> Option<String> {
let bot_id = ctx.cache.current_user().id;
let pool = pool(ctx).await?;
get_command_alias(&pool, bot_id, command)
.await
.ok()
.flatten()
}
pub async fn resolve_command_alias_name(ctx: &Context, command: &str) -> Option<String> {
resolve_alias(ctx, command).await
}
pub struct AliasCommand;
pub static COMMAND_DESCRIPTOR: AliasCommand = AliasCommand;
impl crate::commands::command_contract::CommandSpec for AliasCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "alias",
command: "alias",
category: "permissions",
params: "<commande> <alias> | remove <alias> | list",
summary: "Gere les aliases personnalises",
description: "Liste, ajoute ou supprime des aliases de commandes stockes en base.",
examples: &["+alias", "+as", "+help alias"],
alias_source_key: "alias",
default_aliases: &["als"],
}
}
}
+150
View File
@@ -0,0 +1,150 @@
use serenity::builder::CreateEmbed;
use serenity::model::guild::Role;
use serenity::model::prelude::*;
use serenity::prelude::*;
use std::collections::HashMap;
use crate::commands::common::{add_list_fields, has_flag, mention_user, parse_limit, send_embed};
pub async fn handle_alladmins(ctx: &Context, msg: &Message, args: &[&str]) {
let limit = parse_limit(args, 25, 100);
let detailed = has_flag(args, &["--details", "-d", "full"]);
let Some(guild_id) = msg.guild_id else {
let embed = CreateEmbed::new()
.title("Commande invalide")
.description("Cette commande doit être utilisée dans un serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let partial_guild = match guild_id.to_partial_guild(&ctx.http).await {
Ok(guild) => guild,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer le serveur: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let members = match guild_id.members(&ctx.http, None, None).await {
Ok(members) => members,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer les membres: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let mut admin_members = members
.iter()
.filter(|member| {
!member.user.bot
&& has_admin_permission(member, partial_guild.owner_id, &partial_guild.roles)
})
.collect::<Vec<_>>();
admin_members.sort_by_key(|member| member.user.name.to_lowercase());
if admin_members.is_empty() {
let embed = CreateEmbed::new()
.title("Admins (hors bots)")
.description("Aucun administrateur (hors bots) trouvé.")
.color(0xFEE75C);
send_embed(ctx, msg, embed).await;
return;
}
let visible = admin_members.iter().take(limit).collect::<Vec<_>>();
let lines = visible
.iter()
.map(|member| {
if detailed {
let top_role = member
.roles
.iter()
.filter_map(|role_id| partial_guild.roles.get(role_id))
.max_by_key(|role| role.position)
.map(|role| role.name.clone())
.unwrap_or_else(|| "Aucun".to_string());
format!(
"- {} | ID: {} | Roles: {} | Top: {}",
mention_user(member.user.id),
member.user.id,
member.roles.len(),
top_role
)
} else {
format!(
"- {} (ID: {})",
mention_user(member.user.id),
member.user.id
)
}
})
.collect::<Vec<_>>();
let ratio = (admin_members.len() as f64 / members.len() as f64) * 100.0;
let mut embed = CreateEmbed::new()
.title("Admins (hors bots)")
.description(format!(
"Serveur: **{}**\nAdmins humains: **{}** / Membres: **{}** ({:.1}%)",
partial_guild.name,
admin_members.len(),
members.len(),
ratio
))
.color(0x5865F2)
.field("Owner", partial_guild.owner_id.mention().to_string(), true);
embed = add_list_fields(
embed,
&lines,
&format!(
"Liste ({} affichés / {} total)",
visible.len(),
admin_members.len()
),
);
send_embed(ctx, msg, embed).await;
}
fn has_admin_permission(member: &Member, owner_id: UserId, roles: &HashMap<RoleId, Role>) -> bool {
if member.user.id == owner_id {
return true;
}
member.roles.iter().any(|role_id| {
roles
.get(role_id)
.is_some_and(|role| role.permissions.contains(Permissions::ADMINISTRATOR))
})
}
pub struct AlladminsCommand;
pub static COMMAND_DESCRIPTOR: AlladminsCommand = AlladminsCommand;
impl crate::commands::command_contract::CommandSpec for AlladminsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "alladmins",
command: "alladmins",
category: "general",
params: "aucun",
summary: "Liste les administrateurs du serveur",
description: "Affiche les membres qui possedent des droits administrateur sur le serveur.",
examples: &["+alladmins", "+as", "+help alladmins"],
alias_source_key: "alladmins",
default_aliases: &["aad"],
}
}
}
+133
View File
@@ -0,0 +1,133 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{
add_list_fields, discord_ts, has_flag, mention_user, parse_limit, send_embed,
};
pub async fn handle_allbots(ctx: &Context, msg: &Message, args: &[&str]) {
let limit = parse_limit(args, 25, 100);
let detailed = has_flag(args, &["--details", "-d", "full"]);
let Some(guild_id) = msg.guild_id else {
let embed = CreateEmbed::new()
.title("Commande invalide")
.description("Cette commande doit être utilisée dans un serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let guild = match guild_id.to_partial_guild(&ctx.http).await {
Ok(guild) => guild,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer le serveur: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let members = match guild_id.members(&ctx.http, None, None).await {
Ok(members) => members,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer les membres: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let mut bots = members
.iter()
.filter(|member| member.user.bot)
.collect::<Vec<_>>();
bots.sort_by_key(|member| member.user.name.to_lowercase());
if bots.is_empty() {
let embed = CreateEmbed::new()
.title("Bots du serveur")
.description("Aucun bot trouvé sur ce serveur.")
.color(0xFEE75C);
send_embed(ctx, msg, embed).await;
return;
}
let visible = bots.iter().take(limit).collect::<Vec<_>>();
let lines = visible
.iter()
.map(|member| {
if detailed {
format!(
"- {} | ID: {} | Créé: {}",
mention_user(member.user.id),
member.user.id,
discord_ts(member.user.created_at(), "F")
)
} else {
format!(
"- {} (ID: {})",
mention_user(member.user.id),
member.user.id
)
}
})
.collect::<Vec<_>>();
let bot_ratio = (bots.len() as f64 / members.len() as f64) * 100.0;
let mut embed = CreateEmbed::new()
.title("Bots présents sur le serveur")
.description(format!(
"Serveur: **{}**\nBots: **{}** / Membres: **{}** ({:.1}%)",
guild.name,
bots.len(),
members.len(),
bot_ratio
))
.color(0x5865F2);
if let Some(newest_bot) = bots.iter().max_by_key(|member| member.user.created_at()) {
embed = embed.field(
"Bot le plus récent",
format!(
"{} ({})",
mention_user(newest_bot.user.id),
discord_ts(newest_bot.user.created_at(), "F")
),
false,
);
}
embed = add_list_fields(
embed,
&lines,
&format!("Liste ({} affichés / {} total)", visible.len(), bots.len()),
);
send_embed(ctx, msg, embed).await;
}
pub struct AllbotsCommand;
pub static COMMAND_DESCRIPTOR: AllbotsCommand = AllbotsCommand;
impl crate::commands::command_contract::CommandSpec for AllbotsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "allbots",
command: "allbots",
category: "general",
params: "aucun",
summary: "Liste tous les bots du serveur",
description: "Affiche la liste des membres bots presents sur le serveur courant.",
examples: &["+allbots", "+as", "+help allbots"],
alias_source_key: "allbots",
default_aliases: &["abt"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_allperms(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_allperms(ctx, msg, args).await;
}
pub struct AllpermsCommand;
pub static COMMAND_DESCRIPTOR: AllpermsCommand = AllpermsCommand;
impl crate::commands::command_contract::CommandSpec for AllpermsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "allperms",
command: "allperms",
category: "permissions",
params: "[page]",
summary: "Liste les ACL de toutes commandes",
description: "Affiche le niveau ACL requis pour chaque commande avec pagination.",
examples: &["+allperms", "+as", "+help allperms"],
alias_source_key: "allperms",
default_aliases: &["apm"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_autobackup(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_autobackup(ctx, msg, args).await;
}
pub struct AutoBackupCommand;
pub static COMMAND_DESCRIPTOR: AutoBackupCommand = AutoBackupCommand;
impl crate::commands::command_contract::CommandSpec for AutoBackupCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "autobackup",
command: "autobackup",
category: "admin",
params: "<serveur/emoji> <jours>",
summary: "Configure les backups automatiques",
description: "Definit l'intervalle en jours des backups automatiques.",
examples: &["+autobackup serveur 3", "+autobackup emoji 7"],
alias_source_key: "autobackup",
default_aliases: &["abkp"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_autoconfiglog(ctx: &Context, msg: &Message) {
logs_service::handle_autoconfiglog(ctx, msg).await;
}
pub struct AutoconfiglogCommand;
pub static COMMAND_DESCRIPTOR: AutoconfiglogCommand = AutoconfiglogCommand;
impl crate::commands::command_contract::CommandSpec for AutoconfiglogCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "autoconfiglog",
command: "autoconfiglog",
category: "admin",
params: "aucun",
summary: "Cree tous les salons de logs",
description: "Cree automatiquement les salons de logs et les configure.",
examples: &["+autoconfiglog"],
alias_source_key: "autoconfiglog",
default_aliases: &["acl"],
}
}
}
+112
View File
@@ -0,0 +1,112 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::Colour;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{parse_channel_id, send_embed};
use crate::db;
pub async fn handle_autopublish(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
if args.is_empty() {
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let channels = db::get_autopublish_channels(&pool, bot_id, guild_id.get() as i64)
.await
.unwrap_or_default();
let description = if channels.is_empty() {
"Aucun salon d'annonces configuré.".to_string()
} else {
channels
.into_iter()
.map(|channel| format!("<#{}>", channel.channel_id))
.collect::<Vec<_>>()
.join("\n")
};
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description(description)
.colour(Colour::from_rgb(100, 150, 255)),
)
.await;
return;
}
let enabled = args[0].eq_ignore_ascii_case("on") || args[0].eq_ignore_ascii_case("enable");
let disabled = args[0].eq_ignore_ascii_case("off") || args[0].eq_ignore_ascii_case("disable");
if !enabled && !disabled {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description("Utilisation: +autopublish on|off [#canal]")
.color(0xED4245),
)
.await;
return;
}
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
let channel_id = args
.get(1)
.and_then(|value| parse_channel_id(value))
.unwrap_or(msg.channel_id);
let result = if enabled {
db::add_autopublish_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
} else {
db::remove_autopublish_channel(&pool, bot_id, guild_id_i64, channel_id.get() as i64).await
};
if result.is_err() {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Autopublish")
.description("Impossible de mettre à jour le salon d'annonces.")
.color(0xED4245),
)
.await;
return;
}
let embed = if enabled {
CreateEmbed::new()
.title("Autopublish activé")
.description(format!("Salon: <#{}>", channel_id.get()))
.colour(Colour::from_rgb(0, 200, 120))
.timestamp(Utc::now())
} else {
CreateEmbed::new()
.title("Autopublish désactivé")
.description(format!("Salon: <#{}>", channel_id.get()))
.colour(Colour::from_rgb(255, 120, 0))
.timestamp(Utc::now())
};
send_embed(ctx, msg, embed).await;
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_autoreact(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_autoreact(ctx, msg, args).await;
}
pub struct AutoReactCommand;
pub static COMMAND_DESCRIPTOR: AutoReactCommand = AutoReactCommand;
impl crate::commands::command_contract::CommandSpec for AutoReactCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "autoreact",
command: "autoreact",
category: "admin",
params: "<add/del> <salon> <emoji> | list",
summary: "Configure les reactions automatiques",
description: "Ajoute, retire et liste les reactions automatiquement appliquees aux messages d'un salon.",
examples: &["+autoreact add #general 😀", "+autoreact list"],
alias_source_key: "autoreact",
default_aliases: &["ar", "reactauto"],
}
}
}
+31
View File
@@ -0,0 +1,31 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_backup(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_backup(ctx, msg, args).await;
}
pub struct BackupCommand;
pub static COMMAND_DESCRIPTOR: BackupCommand = BackupCommand;
impl crate::commands::command_contract::CommandSpec for BackupCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "backup",
command: "backup",
category: "admin",
params: "<serveur/emoji> <nom> | list/delete/load",
summary: "Gere les backups serveur et emojis",
description: "Cree, liste, supprime et recharge des backups serveur ou emojis.",
examples: &[
"+backup serveur prod_1",
"+backup list serveur",
"+backup load emoji nightly",
],
alias_source_key: "backup",
default_aliases: &["bkp"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_ban(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_ban(ctx, msg, args, false).await;
}
pub struct BanCommand;
pub static COMMAND_DESCRIPTOR: BanCommand = BanCommand;
impl crate::commands::command_contract::CommandSpec for BanCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "ban",
command: "ban",
category: "admin",
params: "<@membre/ID[,..]> [raison]",
summary: "Bannit un membre",
description: "Ban un ou plusieurs membres.",
examples: &["+ban @User"],
alias_source_key: "ban",
default_aliases: &["b"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_banlist(ctx: &Context, msg: &Message) {
moderation_tools::handle_banlist(ctx, msg).await;
}
pub struct BanlistCommand;
pub static COMMAND_DESCRIPTOR: BanlistCommand = BanlistCommand;
impl crate::commands::command_contract::CommandSpec for BanlistCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "banlist",
command: "banlist",
category: "admin",
params: "aucun",
summary: "Liste les bans",
description: "Affiche la liste des bannissements en cours.",
examples: &["+banlist"],
alias_source_key: "banlist",
default_aliases: &["bls"],
}
}
}
+78
View File
@@ -0,0 +1,78 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
pub async fn handle_banner(ctx: &Context, msg: &Message, args: &[&str]) {
let user = if args.is_empty() {
msg.author.clone()
} else {
let user_id = args[0]
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!')
.parse::<u64>()
.ok()
.map(UserId::new);
if let Some(uid) = user_id {
match ctx.http.get_user(uid).await {
Ok(u) => u,
Err(_) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Utilisateur non trouvé.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
}
} else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible de parser l'utilisateur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let banner_url = user.banner_url().unwrap_or_default();
if banner_url.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("{} n'a pas de bannière.", user.name))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let embed = CreateEmbed::new()
.title(format!("Bannière de {}", user.name))
.image(banner_url)
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
}
pub struct BannerCommand;
pub static COMMAND_DESCRIPTOR: BannerCommand = BannerCommand;
impl crate::commands::command_contract::CommandSpec for BannerCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "banner",
command: "banner",
category: "general",
params: "<@membre/ID>",
summary: "Affiche la banniere utilisateur",
description: "Affiche la banniere de profil dun utilisateur cible ou de lauteur.",
examples: &["+banner", "+br", "+help banner"],
alias_source_key: "banner",
default_aliases: &["bnr"],
}
}
}
+92
View File
@@ -0,0 +1,92 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::{ban_user_everywhere, ensure_owner, parse_user_id};
use crate::commands::common::{add_list_fields, send_embed, theme_color, truncate_text};
use crate::db::{DbPoolKey, add_to_blacklist, list_blacklist};
pub async fn handle_bl(ctx: &Context, msg: &Message, args: &[&str]) {
if ensure_owner(ctx, msg).await.is_err() {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.is_empty() {
let rows = list_blacklist(&pool, bot_id).await.unwrap_or_default();
let lines = rows
.iter()
.map(|r| format!("<@{}> · {}", r.user_id, truncate_text(&r.reason, 80)))
.collect::<Vec<_>>();
let color = theme_color(ctx).await;
let mut embed = serenity::builder::CreateEmbed::new()
.title("Blacklist")
.color(color);
embed = add_list_fields(embed, &lines, "Membres blacklistés");
send_embed(ctx, msg, embed).await;
return;
}
let Some(target) = parse_user_id(args[0]) else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Membre invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let reason = if args.len() > 1 {
args[1..].join(" ")
} else {
"Aucune raison fournie".to_string()
};
let _ = add_to_blacklist(&pool, bot_id, target, &reason, Some(msg.author.id)).await;
let (ok, ko) = ban_user_everywhere(ctx, target, &format!("Blacklist: {}", reason)).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Blacklist mise à jour")
.description(format!("<@{}> a été blacklisté.", target.get()))
.field("Raison", truncate_text(&reason, 1024), false)
.field(
"Bans appliqués",
format!("{} réussis · {} échecs", ok, ko),
false,
)
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub struct BlCommand;
pub static COMMAND_DESCRIPTOR: BlCommand = BlCommand;
impl crate::commands::command_contract::CommandSpec for BlCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "bl",
command: "bl",
category: "admin",
params: "[<@membre/ID> [raison...]]",
summary: "Gere la blacklist globale",
description: "Affiche la blacklist ou ajoute un utilisateur a la blacklist globale du bot.",
examples: &["+bl", "+help bl"],
alias_source_key: "bl",
default_aliases: &["bls"],
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::{ensure_owner, parse_user_id};
use crate::commands::common::{send_embed, truncate_text};
use crate::db::{DbPoolKey, get_blacklist_info};
pub async fn handle_blinfo(ctx: &Context, msg: &Message, args: &[&str]) {
if ensure_owner(ctx, msg).await.is_err() {
return;
}
if args.is_empty() {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+blinfo <@membre/ID>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Some(target) = parse_user_id(args[0]) else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Membre invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let info = get_blacklist_info(&pool, bot_id, target)
.await
.ok()
.flatten();
let Some(info) = info else {
let embed = serenity::builder::CreateEmbed::new()
.title("Blacklist")
.description("Ce membre n'est pas blacklisté.")
.color(0xFF0000);
send_embed(ctx, msg, embed).await;
return;
};
let added_at = crate::commands::common::discord_ts(
Timestamp::from_unix_timestamp(info.added_at.timestamp())
.unwrap_or_else(|_| Timestamp::now()),
"F",
);
let by = info
.added_by
.map(|id| format!("<@{}>", id))
.unwrap_or_else(|| "Inconnu".to_string());
let embed = serenity::builder::CreateEmbed::new()
.title("Informations blacklist")
.field("Membre", format!("<@{}>", info.user_id), true)
.field("Ajouté par", by, true)
.field("Ajouté le", added_at, true)
.field("Raison", truncate_text(&info.reason, 1024), false)
.color(0xFF0000);
send_embed(ctx, msg, embed).await;
}
pub struct BlinfoCommand;
pub static COMMAND_DESCRIPTOR: BlinfoCommand = BlinfoCommand;
impl crate::commands::command_contract::CommandSpec for BlinfoCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "blinfo",
command: "blinfo",
category: "admin",
params: "<@membre/ID>",
summary: "Affiche les details blacklist",
description: "Affiche les details de blacklist pour un utilisateur donne.",
examples: &["+blinfo", "+bo", "+help blinfo"],
alias_source_key: "blinfo",
default_aliases: &["bli"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_boostembed(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_boostembed(ctx, msg, args).await;
}
pub struct BoostembedCommand;
pub static COMMAND_DESCRIPTOR: BoostembedCommand = BoostembedCommand;
impl crate::commands::command_contract::CommandSpec for BoostembedCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "boostembed",
command: "boostembed",
category: "admin",
params: "<on|off|test>",
summary: "Active, coupe ou teste l embed boost",
description: "Controle l embed de boost et permet un test rapide.",
examples: &["+boostembed on", "+boostembed test"],
alias_source_key: "boostembed",
default_aliases: &["bembed"],
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{
add_list_fields, discord_ts, has_flag, mention_user, parse_limit, send_embed,
};
pub async fn handle_boosters(ctx: &Context, msg: &Message, args: &[&str]) {
let limit = parse_limit(args, 25, 100);
let detailed = has_flag(args, &["--details", "-d", "full"]);
let recent_first = has_flag(args, &["--recent", "recent", "-r"]);
let Some(guild_id) = msg.guild_id else {
let embed = CreateEmbed::new()
.title("Commande invalide")
.description("Cette commande doit être utilisée dans un serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let guild = match guild_id.to_partial_guild(&ctx.http).await {
Ok(guild) => guild,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer le serveur: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let members = match guild_id.members(&ctx.http, None, None).await {
Ok(members) => members,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer les membres: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let mut boosters = members
.iter()
.filter(|member| member.premium_since.is_some())
.collect::<Vec<_>>();
if recent_first {
boosters.sort_by(|left, right| right.premium_since.cmp(&left.premium_since));
} else {
boosters.sort_by_key(|member| member.user.name.to_lowercase());
}
if boosters.is_empty() {
let embed = CreateEmbed::new()
.title("Boosters du serveur")
.description("Aucun booster trouvé sur ce serveur.")
.color(0xFEE75C);
send_embed(ctx, msg, embed).await;
return;
}
let visible = boosters.iter().take(limit).collect::<Vec<_>>();
let lines = visible
.iter()
.map(|member| {
if detailed {
format!(
"- {} | ID: {} | Boost depuis: {}",
mention_user(member.user.id),
member.user.id,
member
.premium_since
.map(|value| discord_ts(value, "F"))
.unwrap_or_else(|| "Inconnu".to_string())
)
} else {
format!("- {}", mention_user(member.user.id))
}
})
.collect::<Vec<_>>();
let ratio = (boosters.len() as f64 / members.len() as f64) * 100.0;
let mut embed = CreateEmbed::new()
.title("Membres boostant le serveur")
.description(format!(
"Serveur: **{}**\nBoosters: **{}** / Membres: **{}** ({:.1}%)",
guild.name,
boosters.len(),
members.len(),
ratio
))
.color(0x5865F2);
if let Some(last_boost) = boosters
.iter()
.filter_map(|member| {
member
.premium_since
.map(|since| (mention_user(member.user.id), since))
})
.max_by_key(|(_, since)| *since)
{
embed = embed.field(
"Dernier boost",
format!("{} ({})", last_boost.0, discord_ts(last_boost.1, "F")),
false,
);
}
embed = add_list_fields(
embed,
&lines,
&format!(
"Liste ({} affichés / {} total)",
visible.len(),
boosters.len()
),
);
send_embed(ctx, msg, embed).await;
}
pub struct BoostersCommand;
pub static COMMAND_DESCRIPTOR: BoostersCommand = BoostersCommand;
impl crate::commands::command_contract::CommandSpec for BoostersCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "boosters",
command: "boosters",
category: "general",
params: "aucun",
summary: "Liste les boosters du serveur",
description: "Affiche les membres qui boostent actuellement le serveur.",
examples: &["+boosters", "+bs", "+help boosters"],
alias_source_key: "boosters",
default_aliases: &["bst"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_boostlog(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_log_toggle(ctx, msg, args, "boost", "BoostLog").await;
}
pub struct BoostlogCommand;
pub static COMMAND_DESCRIPTOR: BoostlogCommand = BoostlogCommand;
impl crate::commands::command_contract::CommandSpec for BoostlogCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "boostlog",
command: "boostlog",
category: "admin",
params: "<on [salon]|off>",
summary: "Active les logs de boosts",
description: "Active ou desactive les logs de boosts.",
examples: &["+boostlog on #logs", "+boostlog off"],
alias_source_key: "boostlog",
default_aliases: &["blog"],
}
}
}
+149
View File
@@ -0,0 +1,149 @@
use serenity::builder::CreateEmbed;
use serenity::model::guild::Role;
use serenity::model::prelude::*;
use serenity::prelude::*;
use std::collections::HashMap;
use crate::commands::common::{
add_list_fields, discord_ts, has_flag, mention_user, parse_limit, send_embed,
};
pub async fn handle_botadmins(ctx: &Context, msg: &Message, args: &[&str]) {
let limit = parse_limit(args, 25, 100);
let detailed = has_flag(args, &["--details", "-d", "full"]);
let Some(guild_id) = msg.guild_id else {
let embed = CreateEmbed::new()
.title("Commande invalide")
.description("Cette commande doit être utilisée dans un serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let partial_guild = match guild_id.to_partial_guild(&ctx.http).await {
Ok(guild) => guild,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer le serveur: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let members = match guild_id.members(&ctx.http, None, None).await {
Ok(members) => members,
Err(why) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description(format!("Impossible de récupérer les membres: {why}"))
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let mut admin_bots = members
.iter()
.filter(|member| {
member.user.bot
&& has_admin_permission(member, partial_guild.owner_id, &partial_guild.roles)
})
.collect::<Vec<_>>();
admin_bots.sort_by_key(|member| member.user.name.to_lowercase());
if admin_bots.is_empty() {
let embed = CreateEmbed::new()
.title("Bots administrateurs")
.description("Aucun bot administrateur trouvé.")
.color(0xFEE75C);
send_embed(ctx, msg, embed).await;
return;
}
let visible = admin_bots.iter().take(limit).collect::<Vec<_>>();
let lines = visible
.iter()
.map(|member| {
if detailed {
format!(
"- {} | ID: {} | Roles: {} | Créé: {}",
mention_user(member.user.id),
member.user.id,
member.roles.len(),
discord_ts(member.user.created_at(), "F")
)
} else {
format!(
"- {} (ID: {})",
mention_user(member.user.id),
member.user.id
)
}
})
.collect::<Vec<_>>();
let total_bots = members.iter().filter(|member| member.user.bot).count();
let ratio = if total_bots == 0 {
0.0
} else {
(admin_bots.len() as f64 / total_bots as f64) * 100.0
};
let mut embed = CreateEmbed::new()
.title("Bots administrateurs")
.description(format!(
"Serveur: **{}**\nBots admin: **{}** / Bots totaux: **{}** ({:.1}%)",
partial_guild.name,
admin_bots.len(),
total_bots,
ratio
))
.color(0x5865F2);
embed = add_list_fields(
embed,
&lines,
&format!(
"Liste ({} affichés / {} total)",
visible.len(),
admin_bots.len()
),
);
send_embed(ctx, msg, embed).await;
}
fn has_admin_permission(member: &Member, owner_id: UserId, roles: &HashMap<RoleId, Role>) -> bool {
if member.user.id == owner_id {
return true;
}
member.roles.iter().any(|role_id| {
roles
.get(role_id)
.is_some_and(|role| role.permissions.contains(Permissions::ADMINISTRATOR))
})
}
pub struct BotadminsCommand;
pub static COMMAND_DESCRIPTOR: BotadminsCommand = BotadminsCommand;
impl crate::commands::command_contract::CommandSpec for BotadminsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "botadmins",
command: "botadmins",
category: "general",
params: "aucun",
summary: "Liste les admins du bot",
description: "Affiche les utilisateurs ayant des droits admin sur le bot.",
examples: &["+botadmins", "+bs", "+help botadmins"],
alias_source_key: "botadmins",
default_aliases: &["bad"],
}
}
}
+166
View File
@@ -0,0 +1,166 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::activity::{RotatingActivityKind, parse_status, start_rotation, stop_rotation};
use crate::commands::common::send_embed;
use crate::db::{DbPoolKey, set_bot_status};
pub fn parse_color(value: &str) -> Option<u32> {
let v = value.trim().to_lowercase();
match v.as_str() {
"red" | "rouge" => Some(0xED4245),
"green" | "vert" => Some(0x57F287),
"blue" | "bleu" => Some(0x5865F2),
"yellow" | "jaune" => Some(0xFEE75C),
"orange" => Some(0xFAA61A),
"purple" | "violet" => Some(0x9B59B6),
"pink" | "rose" => Some(0xEB459E),
"white" | "blanc" => Some(0xFFFFFF),
"black" | "noir" => Some(0x000000),
_ => {
let hex = v.trim_start_matches('#').trim_start_matches("0x");
u32::from_str_radix(hex, 16).ok()
}
}
}
pub async fn save_status_if_db(ctx: &Context, status: &str) {
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let _ = set_bot_status(&pool, bot_id, status).await;
}
}
pub async fn handle_activity(ctx: &Context, msg: &Message, command: &str, args: &[&str]) {
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+playto|+listen|+watch|+compet|+stream <message>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Some(kind) = RotatingActivityKind::from_command(command) else {
return;
};
let joined = args.join(" ");
let messages: Vec<String> = joined
.split(",,")
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
if messages.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Aucun message d'activité valide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot_id = ctx.cache.current_user().id;
let status = {
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
if let Ok(Some(saved)) = crate::db::get_bot_status(&pool, bot_id).await {
parse_status(&saved)
} else {
OnlineStatus::Online
}
} else {
OnlineStatus::Online
}
};
start_rotation(ctx, kind, messages.clone(), status).await;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let _ =
crate::db::set_bot_activity(&pool, bot_id, kind.as_db(), &messages.join("\n")).await;
}
let embed = CreateEmbed::new()
.title("Activité mise à jour")
.description(format!("{} message(s) configuré(s).", messages.len()))
.field(
"Rotation",
"Les textes alternent toutes les 30 secondes.",
false,
)
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_remove_activity(ctx: &Context, msg: &Message) {
stop_rotation(ctx).await;
ctx.set_activity(None);
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let _ = crate::db::clear_bot_activity(&pool, bot_id).await;
}
let embed = CreateEmbed::new()
.title("Activité supprimée")
.description("L'activité du bot a été retirée.")
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_status(ctx: &Context, msg: &Message, command: &str) {
let status_name = match command {
"+online" => {
ctx.online();
"online"
}
"+idle" => {
ctx.idle();
"idle"
}
"+dnd" => {
ctx.dnd();
"dnd"
}
"+invisible" => {
ctx.invisible();
"invisible"
}
_ => return,
};
save_status_if_db(ctx, status_name).await;
let embed = CreateEmbed::new()
.title("Statut mis à jour")
.description(format!("Nouveau statut: {}", status_name))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
+46
View File
@@ -0,0 +1,46 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::activity::{RotatingActivityKind, parse_status, start_rotation};
use crate::db::DbPoolKey;
pub async fn restore_presence_from_db(ctx: &Context) {
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
return;
};
let status = match crate::db::get_bot_status(&pool, bot_id).await {
Ok(Some(saved)) => parse_status(&saved),
_ => OnlineStatus::Online,
};
ctx.set_presence(None, status);
let activity_row = crate::db::get_bot_activity(&pool, bot_id)
.await
.ok()
.flatten();
if let Some((kind_raw, messages_raw)) = activity_row {
let Some(kind) = RotatingActivityKind::from_db(&kind_raw) else {
return;
};
let messages: Vec<String> = messages_raw
.split('\n')
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
if !messages.is_empty() {
start_rotation(ctx, kind, messages, status).await;
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_bringall(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_bringall(ctx, msg, args).await;
}
pub struct BringAllCommand;
pub static COMMAND_DESCRIPTOR: BringAllCommand = BringAllCommand;
impl crate::commands::command_contract::CommandSpec for BringAllCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "bringall",
command: "bringall",
category: "admin",
params: "[salon_vocal_destination]",
summary: "Rassemble tous les vocaux",
description: "Deplace tous les membres actuellement en vocal vers un salon cible.",
examples: &["+bringall #Event", "+bringall"],
alias_source_key: "bringall",
default_aliases: &["ball", "vbring"],
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_button(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_button(ctx, msg, args).await;
}
pub struct ButtonCommand;
pub static COMMAND_DESCRIPTOR: ButtonCommand = ButtonCommand;
impl crate::commands::command_contract::CommandSpec for ButtonCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "button",
command: "button",
category: "admin",
params: "<add/del> <lien>",
summary: "Gere des boutons decoratifs",
description: "Ajoute ou supprime un bouton de decoration personnalise sur un message du bot.",
examples: &[
"+button add https://example.com",
"+button del https://example.com",
],
alias_source_key: "button",
default_aliases: &["btn"],
}
}
}
+108
View File
@@ -0,0 +1,108 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
fn parse_linear_expression(expr: &str) -> Option<(f64, f64)> {
let normalized = expr.replace(' ', "").replace('-', "+-");
let mut a = 0.0f64;
let mut b = 0.0f64;
for raw in normalized.split('+') {
let term = raw.trim();
if term.is_empty() {
continue;
}
if term.contains('x') {
let coeff = term.replace('x', "");
let c = if coeff.is_empty() || coeff == "+" {
1.0
} else if coeff == "-" {
-1.0
} else {
coeff.parse::<f64>().ok()?
};
a += c;
} else {
b += term.parse::<f64>().ok()?;
}
}
Some((a, b))
}
fn solve_linear_equation(input: &str) -> Option<String> {
let parts: Vec<&str> = input.split('=').collect();
if parts.len() != 2 {
return None;
}
let (a1, b1) = parse_linear_expression(parts[0])?;
let (a2, b2) = parse_linear_expression(parts[1])?;
let a = a1 - a2;
let b = b2 - b1;
if a.abs() < f64::EPSILON {
if b.abs() < f64::EPSILON {
return Some("Équation indéterminée (infinité de solutions).".to_string());
}
return Some("Équation impossible (aucune solution).".to_string());
}
let x = b / a;
Some(format!("x = {}", x))
}
pub async fn handle_calc(ctx: &Context, msg: &Message, args: &[&str]) {
let color = theme_color(ctx).await;
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+calc <calcul>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let query = args.join(" ");
let result = if query.contains('=') && query.contains('x') {
solve_linear_equation(&query)
.unwrap_or_else(|| "Impossible de résoudre cette équation.".to_string())
} else {
match meval::eval_str(&query) {
Ok(value) => value.to_string(),
Err(_) => "Expression invalide.".to_string(),
}
};
let embed = CreateEmbed::new()
.title("Calcul")
.field("Entrée", query, false)
.field("Résultat", result, false)
.color(color);
send_embed(ctx, msg, embed).await;
}
pub struct CalcCommand;
pub static COMMAND_DESCRIPTOR: CalcCommand = CalcCommand;
impl crate::commands::command_contract::CommandSpec for CalcCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "calc",
command: "calc",
category: "general",
params: "<expression>",
summary: "Calcule une expression",
description: "Evalue une expression numerique simple et renvoie le resultat.",
examples: &["+calc", "+cc", "+help calc"],
alias_source_key: "calc",
default_aliases: &["clc"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_change(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_change(ctx, msg, args).await;
}
pub struct ChangeCommand;
pub static COMMAND_DESCRIPTOR: ChangeCommand = ChangeCommand;
impl crate::commands::command_contract::CommandSpec for ChangeCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "change",
command: "change",
category: "permissions",
params: "<commande> <niveau 0-9> | reset",
summary: "Change un niveau de permission",
description: "Definit le niveau ACL requis pour une commande ou reinitialise les overrides.",
examples: &["+change", "+ce", "+help change"],
alias_source_key: "change",
default_aliases: &["chg"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_changeall(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_changeall(ctx, msg, args).await;
}
pub struct ChangeallCommand;
pub static COMMAND_DESCRIPTOR: ChangeallCommand = ChangeallCommand;
impl crate::commands::command_contract::CommandSpec for ChangeallCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "changeall",
command: "changeall",
category: "permissions",
params: "<niveau_source 0-9> <niveau_cible 0-9>",
summary: "Change des permissions en masse",
description: "Remplace en masse un niveau ACL source par un niveau ACL cible.",
examples: &["+changeall", "+cl", "+help changeall"],
alias_source_key: "changeall",
default_aliases: &["cga"],
}
}
}
+112
View File
@@ -0,0 +1,112 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::parse_channel_id;
use crate::commands::common::{discord_ts, send_embed};
pub async fn handle_channel(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Ok(guild) = guild_id.to_partial_guild(&ctx).await else {
return;
};
let channels = guild.channels(ctx).await.unwrap_or_default();
let channel_id = if !args.is_empty() {
let search = args.join(" ").to_lowercase();
parse_channel_id(args[0]).or_else(|| {
// Chercher par nom de canal
channels
.iter()
.find(|(_, c)| c.name.to_lowercase().contains(&search))
.map(|(id, _)| *id)
})
} else {
Some(msg.channel_id)
};
let Some(channel_id) = channel_id else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible de parser le canal.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Ok(channel) = channel_id.to_channel(&ctx).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Canal non trouvé.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
match channel {
Channel::Guild(gc) => {
let created_at = discord_ts(gc.id.created_at(), "F");
let channel_type = match gc.kind {
ChannelType::Text => "Texte",
ChannelType::Voice => "Vocal",
ChannelType::Private => "Privé",
ChannelType::Category => "Catégorie",
ChannelType::News => "Annonces",
ChannelType::Stage => "Stage",
ChannelType::Directory => "Répertoire",
ChannelType::Forum => "Forum",
ChannelType::Unknown(_) => "Inconnu",
_ => "Autre",
};
let mut embed = CreateEmbed::new()
.title(&gc.name)
.description(format!("ID: `{}`", gc.id.get()))
.color(0x5865F2)
.field("Type", channel_type, true)
.field("Créé", created_at, true);
if let Some(topic) = &gc.topic {
embed = embed.field("Sujet", topic, false);
}
if let Some(bitrate) = gc.bitrate {
embed = embed.field("Bitrate", format!("{} kbps", bitrate / 1000), true);
}
embed = embed.field("NSFW", if gc.nsfw { "Oui" } else { "Non" }, true);
send_embed(ctx, msg, embed).await;
}
_ => {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Ce type de canal n'est pas supporté.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
}
}
}
pub struct ChannelCommand;
pub static COMMAND_DESCRIPTOR: ChannelCommand = ChannelCommand;
impl crate::commands::command_contract::CommandSpec for ChannelCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "channel",
command: "channel",
category: "general",
params: "<#salon/ID>",
summary: "Affiche les details dun salon",
description: "Affiche les informations utiles dun salon texte ou vocal cible.",
examples: &["+channel", "+cl", "+help channel"],
alias_source_key: "channel",
default_aliases: &["chl"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_choose(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_choose(ctx, msg, args).await;
}
pub struct ChooseCommand;
pub static COMMAND_DESCRIPTOR: ChooseCommand = ChooseCommand;
impl crate::commands::command_contract::CommandSpec for ChooseCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "choose",
command: "choose",
category: "general",
params: "<option1 | option2 | ...>",
summary: "Tire une option au hasard",
description: "Lance un tirage au sort instantane parmi les options donnees.",
examples: &["+choose rouge | bleu | vert"],
alias_source_key: "choose",
default_aliases: &["pick", "random"],
}
}
}
+69
View File
@@ -0,0 +1,69 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::Colour;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_claim(ctx: &Context, msg: &Message, _args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
let channel_id = msg.channel_id.get() as i64;
let Some(ticket) = db::get_ticket_by_channel(&pool, bot_id, guild_id_i64, channel_id)
.await
.ok()
.flatten()
else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Erreur")
.description("Ce salon n'est pas reconnu comme un ticket.")
.color(0xED4245),
)
.await;
return;
};
if db::claim_ticket(&pool, ticket.id, msg.author.id.get() as i64)
.await
.is_err()
{
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Erreur")
.description("Impossible de revendiquer ce ticket.")
.color(0xED4245),
)
.await;
return;
}
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Ticket revendiqué")
.description(format!("Le ticket #{} a été revendiqué.", ticket.id))
.colour(Colour::from_rgb(0, 200, 120))
.timestamp(Utc::now()),
)
.await;
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_cleanup(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_cleanup(ctx, msg, args).await;
}
pub struct CleanupCommand;
pub static COMMAND_DESCRIPTOR: CleanupCommand = CleanupCommand;
impl crate::commands::command_contract::CommandSpec for CleanupCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "cleanup",
command: "cleanup",
category: "admin",
params: "<salon_vocal>",
summary: "Vide un salon vocal",
description: "Deconnecte tous les utilisateurs presents dans un salon vocal cible.",
examples: &["+cleanup #General"],
alias_source_key: "cleanup",
default_aliases: &["vclean", "vcleanup"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::moderation_tools;
pub async fn handle_clear_all_sanctions(ctx: &Context, msg: &Message) {
moderation_tools::handle_clear_all_sanctions(ctx, msg).await;
}
pub struct ClearAllSanctionsCommand;
pub static COMMAND_DESCRIPTOR: ClearAllSanctionsCommand = ClearAllSanctionsCommand;
impl crate::commands::command_contract::CommandSpec for ClearAllSanctionsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_all_sanctions",
command: "clear all sanctions",
category: "admin",
params: "aucun",
summary: "Supprime toutes les sanctions du serveur",
description: "Efface toutes les sanctions de tous les membres du serveur.",
examples: &["+clear all sanctions"],
alias_source_key: "clear_all_sanctions",
default_aliases: &["casanctions"],
}
}
}
+53
View File
@@ -0,0 +1,53 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::ensure_owner;
use crate::commands::common::send_embed;
use crate::db::{DbPoolKey, clear_blacklist};
pub async fn handle_clear_bl(ctx: &Context, msg: &Message) {
if ensure_owner(ctx, msg).await.is_err() {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let count = clear_blacklist(&pool, bot_id).await.unwrap_or(0);
let embed = serenity::builder::CreateEmbed::new()
.title("Blacklist réinitialisée")
.description(format!("{} membre(s) retiré(s) de la blacklist.", count))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub struct ClearBlCommand;
pub static COMMAND_DESCRIPTOR: ClearBlCommand = ClearBlCommand;
impl crate::commands::command_contract::CommandSpec for ClearBlCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_bl",
command: "clear bl",
category: "admin",
params: "aucun",
summary: "Vide la blacklist globale",
description: "Supprime toutes les entrees de la blacklist globale.",
examples: &["+clear bl", "+cl", "+help clear bl"],
alias_source_key: "clear_bl",
default_aliases: &["cbl"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::moderation_tools;
pub async fn handle_clear_messages(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_clear_messages(ctx, msg, args).await;
}
pub struct ClearMessagesCommand;
pub static COMMAND_DESCRIPTOR: ClearMessagesCommand = ClearMessagesCommand;
impl crate::commands::command_contract::CommandSpec for ClearMessagesCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_messages",
command: "clear",
category: "admin",
params: "<nombre> [@membre/ID]",
summary: "Supprime des messages dans le salon",
description: "Supprime un nombre de messages, optionnellement filtres par membre.",
examples: &["+clear 20", "+clear 20 @User"],
alias_source_key: "clear_messages",
default_aliases: &["purge"],
}
}
}
+53
View File
@@ -0,0 +1,53 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::ensure_owner;
use crate::commands::common::send_embed;
use crate::db::{DbPoolKey, clear_bot_owners};
pub async fn handle_clear_owners(ctx: &Context, msg: &Message) {
if ensure_owner(ctx, msg).await.is_err() {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let count = clear_bot_owners(&pool, bot_id).await.unwrap_or(0);
let embed = serenity::builder::CreateEmbed::new()
.title("Owners réinitialisés")
.description(format!("{} owner(s) supprimé(s).", count))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub struct ClearOwnersCommand;
pub static COMMAND_DESCRIPTOR: ClearOwnersCommand = ClearOwnersCommand;
impl crate::commands::command_contract::CommandSpec for ClearOwnersCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_owners",
command: "clear owners",
category: "admin",
params: "aucun",
summary: "Vide la liste des owners",
description: "Supprime tous les owners supplementaires en base de donnees.",
examples: &["+clear owners", "+cs", "+help clear owners"],
alias_source_key: "clear_owners",
default_aliases: &["cro"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_clear_perms(ctx: &Context, msg: &Message) {
perms_service::handle_clear_perms(ctx, msg).await;
}
pub struct ClearPermsCommand;
pub static COMMAND_DESCRIPTOR: ClearPermsCommand = ClearPermsCommand;
impl crate::commands::command_contract::CommandSpec for ClearPermsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_perms",
command: "clear perms",
category: "permissions",
params: "aucun",
summary: "Vide toutes les permissions scope",
description: "Supprime toutes les permissions ACL configurees en base.",
examples: &["+clear perms", "+cs", "+help clear perms"],
alias_source_key: "clear_perms",
default_aliases: &["cpm"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::moderation_tools;
pub async fn handle_clear_sanctions(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_clear_sanctions(ctx, msg, args).await;
}
pub struct ClearSanctionsCommand;
pub static COMMAND_DESCRIPTOR: ClearSanctionsCommand = ClearSanctionsCommand;
impl crate::commands::command_contract::CommandSpec for ClearSanctionsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "clear_sanctions",
command: "clear sanctions",
category: "admin",
params: "<@membre/ID>",
summary: "Supprime toutes les sanctions d un membre",
description: "Efface completement les sanctions d un membre cible.",
examples: &["+clear sanctions @User"],
alias_source_key: "clear_sanctions",
default_aliases: &["csanctions"],
}
}
}
+76
View File
@@ -0,0 +1,76 @@
use chrono::Utc;
use serenity::builder::CreateEmbed;
use serenity::model::Colour;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::db;
pub async fn handle_close(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let reason = if args.is_empty() {
None
} else {
Some(args.join(" "))
};
let Some(pool) = ({
let data = ctx.data.read().await;
data.get::<db::DbPoolKey>().cloned()
}) else {
return;
};
let bot_id = ctx.cache.current_user().id.get() as i64;
let guild_id_i64 = guild_id.get() as i64;
let channel_id = msg.channel_id.get() as i64;
let Some(ticket) = db::get_ticket_by_channel(&pool, bot_id, guild_id_i64, channel_id)
.await
.ok()
.flatten()
else {
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Erreur")
.description("Ce salon n'est pas reconnu comme un ticket.")
.color(0xED4245),
)
.await;
return;
};
if db::close_ticket(&pool, ticket.id, reason.clone())
.await
.is_err()
{
send_embed(
ctx,
msg,
CreateEmbed::new()
.title("Erreur")
.description("Impossible de fermer ce ticket.")
.color(0xED4245),
)
.await;
return;
}
let mut embed = CreateEmbed::new()
.title("Ticket fermé")
.description(format!("Le ticket #{} a été fermé.", ticket.id))
.colour(Colour::from_rgb(255, 120, 0))
.timestamp(Utc::now());
if let Some(reason) = reason {
embed = embed.field("Raison", reason, false);
}
send_embed(ctx, msg, embed).await;
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_cmute(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_cmute(ctx, msg, args, false).await;
}
pub struct CmuteCommand;
pub static COMMAND_DESCRIPTOR: CmuteCommand = CmuteCommand;
impl crate::commands::command_contract::CommandSpec for CmuteCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "cmute",
command: "cmute",
category: "admin",
params: "<@membre/ID[,..]> [raison]",
summary: "Mute salon",
description: "Mute un membre sur le salon courant.",
examples: &["+cmute @User"],
alias_source_key: "cmute",
default_aliases: &["cm"],
}
}
}
+16
View File
@@ -0,0 +1,16 @@
#[derive(Clone, Copy)]
pub struct CommandMetadata {
pub key: &'static str,
pub command: &'static str,
pub category: &'static str,
pub params: &'static str,
pub summary: &'static str,
pub description: &'static str,
pub examples: &'static [&'static str],
pub alias_source_key: &'static str,
pub default_aliases: &'static [&'static str],
}
pub trait CommandSpec: Send + Sync {
fn metadata(&self) -> CommandMetadata;
}
+141
View File
@@ -0,0 +1,141 @@
use serenity::builder::{CreateEmbed, CreateMessage};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::db::{DbPoolKey, get_bot_theme};
pub fn parse_limit(args: &[&str], default: usize, max: usize) -> usize {
args.iter()
.find_map(|arg| arg.parse::<usize>().ok())
.map(|value| value.clamp(1, max))
.unwrap_or(default)
}
pub fn has_flag(args: &[&str], names: &[&str]) -> bool {
args.iter()
.any(|arg| names.iter().any(|name| arg.eq_ignore_ascii_case(name)))
}
pub fn truncate_text(input: &str, max_len: usize) -> String {
if input.chars().count() <= max_len {
return input.to_string();
}
let mut out = input
.chars()
.take(max_len.saturating_sub(1))
.collect::<String>();
out.push('…');
out
}
pub fn add_list_fields(mut embed: CreateEmbed, lines: &[String], base_name: &str) -> CreateEmbed {
if lines.is_empty() {
return embed.field(base_name, "Aucun résultat.", false);
}
let max_fields = 3;
let chunk_size = 12;
for (index, chunk) in lines.chunks(chunk_size).take(max_fields).enumerate() {
let field_name = if index == 0 {
base_name.to_string()
} else {
format!("{} (suite {})", base_name, index + 1)
};
let value = truncate_text(&chunk.join("\n"), 1024);
embed = embed.field(field_name, value, false);
}
let shown = (chunk_size * max_fields).min(lines.len());
if lines.len() > shown {
embed = embed.field(
"Affichage",
format!("{} éléments affichés sur {}.", shown, lines.len()),
false,
);
}
embed
}
pub fn mention_user(user_id: UserId) -> String {
format!("<@{}>", user_id.get())
}
pub fn discord_ts(timestamp: Timestamp, style: &str) -> String {
format!("<t:{}:{}>", timestamp.unix_timestamp(), style)
}
pub async fn send_embed(ctx: &Context, msg: &Message, embed: CreateEmbed) {
let color = theme_color(ctx).await;
let embed = embed.color(color);
let _ = msg
.channel_id
.send_message(&ctx.http, CreateMessage::new().embed(embed))
.await;
}
pub async fn theme_color(ctx: &Context) -> u32 {
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
if let Ok(Some(color)) = get_bot_theme(&pool, bot_id).await {
return color;
}
}
0xFF0000
}
pub fn parse_role(guild: &PartialGuild, input: &str) -> Option<Role> {
// Essayer de parser comme mention <@&id>
if let Ok(id) = input
.trim_start_matches("<@&")
.trim_end_matches('>')
.parse::<u64>()
{
if let Some(role) = guild.roles.get(&RoleId::new(id)) {
return Some(role.clone());
}
}
// Essayer de parser comme ID brut
if let Ok(id) = input.parse::<u64>() {
if let Some(role) = guild.roles.get(&RoleId::new(id)) {
return Some(role.clone());
}
}
// Chercher par nom (case-insensitive)
let search = input.to_lowercase();
guild
.roles
.values()
.find(|r| r.name.to_lowercase().contains(&search))
.cloned()
}
pub fn parse_channel_id(input: &str) -> Option<ChannelId> {
// Essayer de parser comme mention <#id>
if let Ok(id) = input
.trim_start_matches("<#")
.trim_end_matches('>')
.parse::<u64>()
{
return Some(ChannelId::new(id));
}
// Essayer de parser comme ID brut
if let Ok(id) = input.parse::<u64>() {
return Some(ChannelId::new(id));
}
None
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_compet(ctx: &Context, msg: &Message, args: &[&str]) {
botconfig_common::handle_activity(ctx, msg, "+compet", args).await;
}
pub struct CompetCommand;
pub static COMMAND_DESCRIPTOR: CompetCommand = CompetCommand;
impl crate::commands::command_contract::CommandSpec for CompetCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "compet",
command: "compet",
category: "profile",
params: "<texte[, ,texte2,...]>",
summary: "Definit une activite competing",
description: "Configure la rotation des messages d activite en mode competing.",
examples: &["+compet", "+ct", "+help compet"],
alias_source_key: "compet",
default_aliases: &["cpt"],
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_create(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_create(ctx, msg, args).await;
}
pub struct CreateCommand;
pub static COMMAND_DESCRIPTOR: CreateCommand = CreateCommand;
impl crate::commands::command_contract::CommandSpec for CreateCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "create",
command: "create",
category: "admin",
params: "[emoji/url] [nom]",
summary: "Cree un emoji custom",
description: "Cree un emoji custom a partir d'une image, d'un lien ou d'un emoji nitro.",
examples: &[
"+create <:blob:123456789012345678> blobcopy",
"+create https://... logo",
],
alias_source_key: "create",
default_aliases: &["mkemoji", "ce"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_del(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_del_perm(ctx, msg, args).await;
}
pub struct DelCommand;
pub static COMMAND_DESCRIPTOR: DelCommand = DelCommand;
impl crate::commands::command_contract::CommandSpec for DelCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "del",
command: "del",
category: "permissions",
params: "perm <@&rôle/@membre/ID>",
summary: "Supprime des permissions scope",
description: "Supprime les permissions ACL associees a un role ou utilisateur.",
examples: &["+del", "+dl", "+help del"],
alias_source_key: "del",
default_aliases: &["dlp"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::moderation_tools;
pub async fn handle_del_sanction(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_del_sanction(ctx, msg, args).await;
}
pub struct DelSanctionCommand;
pub static COMMAND_DESCRIPTOR: DelSanctionCommand = DelSanctionCommand;
impl crate::commands::command_contract::CommandSpec for DelSanctionCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "del_sanction",
command: "del sanction",
category: "admin",
params: "<@membre/ID> <nombre>",
summary: "Supprime une sanction d un membre",
description: "Supprime une sanction specifique dans l historique d un membre.",
examples: &["+del sanction @User 1"],
alias_source_key: "del_sanction",
default_aliases: &["delsanction"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_delrole(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_add_del_role(ctx, msg, args, false).await;
}
pub struct DelroleCommand;
pub static COMMAND_DESCRIPTOR: DelroleCommand = DelroleCommand;
impl crate::commands::command_contract::CommandSpec for DelroleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "delrole",
command: "delrole",
category: "admin",
params: "<@membre/ID[,..]> <@role/ID>",
summary: "Retire un role",
description: "Retire un role a un ou plusieurs membres.",
examples: &["+delrole @User @Membre"],
alias_source_key: "delrole",
default_aliases: &["dr"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_derank(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_derank(ctx, msg, args).await;
}
pub struct DerankCommand;
pub static COMMAND_DESCRIPTOR: DerankCommand = DerankCommand;
impl crate::commands::command_contract::CommandSpec for DerankCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "derank",
command: "derank",
category: "admin",
params: "<@membre/ID[,..]>",
summary: "Retire tous les roles",
description: "Retire tous les roles gerables d un membre.",
examples: &["+derank @User"],
alias_source_key: "derank",
default_aliases: &["drk"],
}
}
}
+77
View File
@@ -0,0 +1,77 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::commands::server::resolve_guild_target;
pub async fn handle_discussion(ctx: &Context, msg: &Message, args: &[&str]) {
if args.len() < 2 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+discussion <ID/nombre> <message>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Some(guild_id) = resolve_guild_target(ctx, args[0]).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Serveur introuvable.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let content = args[1..].join(" ");
let Ok(channels) = guild_id.channels(&ctx.http).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible de lire les salons.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
for channel in channels.values() {
if matches!(channel.kind, ChannelType::Text | ChannelType::News) {
let _ = channel
.say(
&ctx.http,
format!("[Discussion via {}] {}", msg.author.tag(), content),
)
.await;
let embed = CreateEmbed::new()
.title("Discussion envoyée")
.description(format!("Message transmis dans `{}`.", guild_id.get()))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
return;
}
}
let embed = CreateEmbed::new()
.title("Erreur")
.description("Aucun salon texte trouvable sur ce serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
}
pub struct DiscussionCommand;
pub static COMMAND_DESCRIPTOR: DiscussionCommand = DiscussionCommand;
impl crate::commands::command_contract::CommandSpec for DiscussionCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "discussion",
command: "discussion",
category: "profile",
params: "<ID_serveur/index> <message...>",
summary: "Diffuse un message serveur",
description: "Envoie un message de discussion sur un serveur cible.",
examples: &["+discussion", "+dn", "+help discussion"],
alias_source_key: "discussion",
default_aliases: &["dsc"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_dnd(ctx: &Context, msg: &Message) {
botconfig_common::handle_status(ctx, msg, "+dnd").await;
}
pub struct DndCommand;
pub static COMMAND_DESCRIPTOR: DndCommand = DndCommand;
impl crate::commands::command_contract::CommandSpec for DndCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "dnd",
command: "dnd",
category: "profile",
params: "aucun",
summary: "Passe le bot en dnd",
description: "Change le statut du bot en do not disturb et sauvegarde ce statut.",
examples: &["+dnd", "+dd", "+help dnd"],
alias_source_key: "dnd",
default_aliases: &["dnm"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_embed(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_embed_builder(ctx, msg, args).await;
}
pub struct EmbedCommand;
pub static COMMAND_DESCRIPTOR: EmbedCommand = EmbedCommand;
impl crate::commands::command_contract::CommandSpec for EmbedCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "embed",
command: "embed",
category: "admin",
params: "title | description (v1)",
summary: "Ouvre le generateur d'embed",
description: "Affiche un generateur d'embed interactif version rapide.",
examples: &["+embed"],
alias_source_key: "embed",
default_aliases: &["emb"],
}
}
}
+97
View File
@@ -0,0 +1,97 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{send_embed, theme_color};
fn parse_custom_emoji(input: &str) -> Option<(bool, String)> {
if !(input.starts_with("<:") || input.starts_with("<a:")) || !input.ends_with('>') {
return None;
}
let animated = input.starts_with("<a:");
let inner = input.trim_start_matches('<').trim_end_matches('>');
let parts: Vec<&str> = inner.split(':').collect();
if parts.len() != 3 {
return None;
}
let id = parts[2].to_string();
Some((animated, id))
}
fn unicode_emoji_url(input: &str) -> Option<String> {
if input.is_empty() {
return None;
}
let codepoints = input
.chars()
.filter(|c| *c as u32 != 0xFE0F)
.map(|c| format!("{:x}", c as u32))
.collect::<Vec<_>>()
.join("-");
if codepoints.is_empty() {
return None;
}
Some(format!(
"https://twemoji.maxcdn.com/v/latest/72x72/{}.png",
codepoints
))
}
pub async fn handle_emoji(ctx: &Context, msg: &Message, args: &[&str]) {
let color = theme_color(ctx).await;
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+emoji <émoji>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let input = args.join(" ");
let url = if let Some((animated, id)) = parse_custom_emoji(&input) {
let ext = if animated { "gif" } else { "png" };
format!("https://cdn.discordapp.com/emojis/{}.{}?size=1024", id, ext)
} else if let Some(url) = unicode_emoji_url(input.trim()) {
url
} else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Émoji invalide. Utilise un émoji Unicode ou un émoji custom Discord.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let embed = CreateEmbed::new()
.title("Image de l'émoji")
.image(url)
.color(color);
send_embed(ctx, msg, embed).await;
}
pub struct EmojiCommand;
pub static COMMAND_DESCRIPTOR: EmojiCommand = EmojiCommand;
impl crate::commands::command_contract::CommandSpec for EmojiCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "emoji",
command: "emoji",
category: "general",
params: "<emoji>",
summary: "Affiche les infos dun emoji",
description: "Affiche les details dun emoji fourni.",
examples: &["+emoji", "+ei", "+help emoji"],
alias_source_key: "emoji",
default_aliases: &["emj"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_end(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_end(ctx, msg, args).await;
}
pub struct EndCommand;
pub static COMMAND_DESCRIPTOR: EndCommand = EndCommand;
impl crate::commands::command_contract::CommandSpec for EndCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "end",
command: "end",
category: "admin",
params: "giveaway <id_message>",
summary: "Termine un giveaway par ID",
description: "Permet de stopper instantanement un giveaway avec l'identifiant du message.",
examples: &["+end giveaway 123456789012345678"],
alias_source_key: "end",
default_aliases: &["gend"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_giveaway(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_giveaway(ctx, msg, args).await;
}
pub struct GiveawayCommand;
pub static COMMAND_DESCRIPTOR: GiveawayCommand = GiveawayCommand;
impl crate::commands::command_contract::CommandSpec for GiveawayCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "giveaway",
command: "giveaway",
category: "admin",
params: "aucun",
summary: "Ouvre un menu de creation de giveaway",
description: "Affiche une interface rapide pour initier un giveaway depuis le salon courant.",
examples: &["+giveaway"],
alias_source_key: "giveaway",
default_aliases: &["gstart", "gw"],
}
}
}
+799
View File
@@ -0,0 +1,799 @@
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::{add_list_fields, truncate_text};
use crate::db::{
DbPoolKey, get_help_aliases_enabled, get_help_perms_enabled, get_help_type,
list_command_aliases,
};
use crate::permissions;
#[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,
params: &'static str,
summary: &'static str,
description: &'static str,
examples: &'static [&'static str],
alias_source_key: Option<&'static str>,
}
#[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.key {
"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" | "helptype"
| "helpalias" => "permissions",
_ => match meta.category {
"general" => "infos",
"profile" => "bot",
"admin" => "administration",
"permissions" => "permissions",
_ => "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.key.eq_ignore_ascii_case(&normalized)
|| meta.key.eq_ignore_ascii_case(&underscored)
|| meta.command.eq_ignore_ascii_case(&normalized)
})
.map(|meta| meta.key)
}
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.alias_source_key.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.key,
command: meta.command,
params: meta.params,
summary: meta.summary,
description: meta.description,
examples: if meta.examples.is_empty() {
HELP_FALLBACK_EXAMPLES
} else {
meta.examples
},
alias_source_key: Some(meta.alias_source_key),
})
}
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" => 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.command.to_lowercase().cmp(&b.command.to_lowercase()));
let mut lines = Vec::with_capacity(commands.len());
for meta in commands {
let label = meta.command;
let summary = meta.summary;
let alias_key = meta.alias_source_key;
let permission = if perms_enabled {
format!(" {}", format_permission_level(permissions::default_permission(meta.key)))
} else {
String::new()
};
if aliases_enabled {
if let Some(aliases) = alias_map.get(alias_key) {
if aliases.is_empty() {
lines.push(format!("`+{}`{} - {}", label, permission, summary));
} else {
lines.push(format!(
"`+{}`{} - {} · alias: `{}`",
label, permission, summary,
aliases.join("`, `")
));
}
continue;
}
}
lines.push(format!("`+{}`{} - {}", label, permission, summary));
}
if lines.is_empty() {
lines.push("Aucune commande dans cette catégorie.".to_string());
}
lines
}
fn build_help_embed(
page_index: usize,
state: &HelpState,
alias_map: &BTreeMap<String, Vec<String>>,
) -> CreateEmbed {
let page = &HELP_PAGES[page_index];
let lines = help_page_content(page, alias_map, state.aliases_enabled, state.perms_enabled);
let mut embed = CreateEmbed::new()
.title(format!("Aide · {}", page.title))
.description(format!(
"Page {}/{} · mode `{}` · aliases {} · perms {}\n{}",
page_index + 1,
HELP_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"
},
page.description,
))
.color(0x5865F2);
embed = add_list_fields(embed, &lines, "Commandes");
embed
}
fn help_components(owner_id: UserId, page_index: usize, state: &HelpState) -> Vec<CreateActionRow> {
let total = HELP_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 options = HELP_PAGES
.iter()
.enumerate()
.map(|(index, page)| {
CreateSelectMenuOption::new(page.title, index.to_string())
.description(truncate_text(page.description, 100))
})
.collect::<Vec<_>>();
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 embed = build_help_embed(0, &state, &alias_map);
let components = help_components(command.user.id, 0, &state);
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 page_index = match kind {
"nav" => page.unwrap_or(0).min(HELP_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(HELP_PAGES.len().saturating_sub(1)),
_ => 0,
},
_ => 0,
};
let embed = build_help_embed(page_index, &state, &alias_map);
let components = help_components(component.user.id, page_index, &state);
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 embed = CreateEmbed::new()
.title(format!("Aide commande · +{}", doc.command))
.description(doc.description)
.field("Commande", format!("`+{}`", doc.command), false)
.field("Clé ACL", format!("`{}`", doc.key), false)
.field("Catégorie", help_page_title_for_command_key(doc.key), false)
.field("Permission", permission_level_description(permissions::default_permission(doc.key)), false)
.field("Alias", alias_text, false)
.field("Paramètres", doc.params, false)
.field("Résumé", doc.summary, false)
.field("Exemples", truncate_text(&examples, 1024), false)
.color(crate::commands::common::theme_color(ctx).await);
let _ = msg
.channel_id
.send_message(&ctx.http, CreateMessage::new().embed(embed))
.await;
return;
}
}
}
let page_index = args
.first()
.and_then(|input| help_page_from_input(input))
.unwrap_or(0)
.min(HELP_PAGES.len().saturating_sub(1));
let embed = build_help_embed(page_index, &state, &alias_map);
let components = help_components(msg.author.id, page_index, &state);
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 {
key: "help",
command: "help",
category: "general",
params: "[commande|page]",
summary: "Affiche laide des commandes",
description: "Affiche les pages daide du bot ou la fiche detaillee dune commande avec parametres, aliases et exemples.",
examples: &["+help", "+hp", "+help help"],
alias_source_key: "help",
default_aliases: &["hp"],
}
}
}
+81
View File
@@ -0,0 +1,81 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::db::{DbPoolKey, get_help_aliases_enabled, set_help_aliases_enabled};
pub async fn handle_helpalias(ctx: &Context, msg: &Message, args: &[&str]) {
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.is_empty() {
let enabled = get_help_aliases_enabled(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or(true);
let embed = serenity::builder::CreateEmbed::new()
.title("Aliases dans help")
.description(format!(
"État actuel: `{}`",
if enabled { "on" } else { "off" }
))
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
return;
}
let enabled = match args[0].to_lowercase().as_str() {
"on" | "true" | "yes" => true,
"off" | "false" | "no" => false,
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+helpalias <on/off>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_help_aliases_enabled(&pool, bot_id, enabled).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Help aliases mis à jour")
.description(format!(
"Aliases dans l'aide: `{}`",
if enabled { "on" } else { "off" }
))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub struct HelpaliasCommand;
pub static COMMAND_DESCRIPTOR: HelpaliasCommand = HelpaliasCommand;
impl crate::commands::command_contract::CommandSpec for HelpaliasCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "helpalias",
command: "helpalias",
category: "permissions",
params: "<on|off>",
summary: "Active ou coupe les aliases help",
description: "Active ou desactive laffichage des aliases dans laide.",
examples: &["+helpalias", "+hs", "+help helpalias"],
alias_source_key: "helpalias",
default_aliases: &["hal"],
}
}
}
+180
View File
@@ -0,0 +1,180 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::db::{
DbPoolKey, get_help_aliases_enabled, get_help_perms_enabled, get_help_type,
set_help_aliases_enabled, set_help_perms_enabled, set_help_type,
};
pub async fn handle_helpsetting(ctx: &Context, msg: &Message, args: &[&str]) {
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.is_empty() {
let help_type = get_help_type(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or_else(|| "button".to_string());
let help_aliases = get_help_aliases_enabled(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or(true);
let help_perms = get_help_perms_enabled(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or(true);
let embed = serenity::builder::CreateEmbed::new()
.title("Configuration de l'aide")
.description("Paramètres actuels:")
.field("Mode d'affichage", format!("`{}`", help_type), true)
.field("Aliases", format!("`{}`", if help_aliases { "on" } else { "off" }), true)
.field(
"Permissions",
format!("`{}`", if help_perms { "on" } else { "off" }),
true,
)
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
return;
}
match args[0].to_lowercase().as_str() {
"type" | "mode" => {
if args.len() < 2 {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+helpsetting type <button|select|hybrid>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let normalized = match args[1].to_lowercase().as_str() {
"button" => "button",
"select" => "select",
"hybrid" => "hybrid",
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Valeurs valides: `button`, `select`, `hybrid`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_help_type(&pool, bot_id, normalized).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Mode de help mis à jour")
.description(format!("Nouveau mode: `{}`", normalized))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
"aliases" | "alias" => {
if args.len() < 2 {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+helpsetting aliases <on|off>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let enabled = match args[1].to_lowercase().as_str() {
"on" | "true" | "yes" => true,
"off" | "false" | "no" => false,
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Valeurs valides: `on`, `off`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_help_aliases_enabled(&pool, bot_id, enabled).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Aliases de help mis à jour")
.description(format!("Aliases: `{}`", if enabled { "on" } else { "off" }))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
"perms" | "permissions" => {
if args.len() < 2 {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+helpsetting perms <on|off>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let enabled = match args[1].to_lowercase().as_str() {
"on" | "true" | "yes" => true,
"off" | "false" | "no" => false,
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Valeurs valides: `on`, `off`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_help_perms_enabled(&pool, bot_id, enabled).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Affichage des permissions mis à jour")
.description(format!(
"Permissions: `{}`",
if enabled { "on" } else { "off" }
))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Sous-commandes: `type`, `aliases`, `perms`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
}
}
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub struct HelpsettingCommand;
pub static COMMAND_DESCRIPTOR: HelpsettingCommand = HelpsettingCommand;
impl crate::commands::command_contract::CommandSpec for HelpsettingCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "helpsetting",
command: "helpsetting",
category: "permissions",
params: "<type|aliases|perms> [value]",
summary: "Configure l'affichage du système d'aide",
description: "Permet de configurer le mode d'affichage, l'affichage des alias et l'affichage des permissions du système d'aide.",
examples: &["+helpsetting", "+helpsetting type hybrid", "+helpsetting perms off"],
alias_source_key: "helpsetting",
default_aliases: &["hs"],
}
}
}
+78
View File
@@ -0,0 +1,78 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::db::{DbPoolKey, get_help_type, set_help_type};
pub async fn handle_helptype(ctx: &Context, msg: &Message, args: &[&str]) {
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.is_empty() {
let current = get_help_type(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or_else(|| "button".to_string());
let embed = serenity::builder::CreateEmbed::new()
.title("Mode help")
.description(format!(
"Mode actuel: `{}`\nValeurs: `button`, `select`, `hybrid`",
current
))
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
return;
}
let normalized = match args[0].to_lowercase().as_str() {
"button" => "button",
"select" => "select",
"hybrid" => "hybrid",
_ => {
let embed = serenity::builder::CreateEmbed::new()
.title("Erreur")
.description("Usage: `+helptype <button/select/hybrid>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_help_type(&pool, bot_id, normalized).await;
let embed = serenity::builder::CreateEmbed::new()
.title("Mode help mis à jour")
.description(format!("Nouveau mode: `{}`", normalized))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
pub struct HelptypeCommand;
pub static COMMAND_DESCRIPTOR: HelptypeCommand = HelptypeCommand;
impl crate::commands::command_contract::CommandSpec for HelptypeCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "helptype",
command: "helptype",
category: "permissions",
params: "<button|select|hybrid>",
summary: "Change le mode daffichage help",
description: "Definit le mode daffichage de laide entre button, select et hybrid.",
examples: &["+helptype", "+he", "+help helptype"],
alias_source_key: "helptype",
default_aliases: &["htp"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_hide(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_hide_unhide(ctx, msg, args, true).await;
}
pub struct HideCommand;
pub static COMMAND_DESCRIPTOR: HideCommand = HideCommand;
impl crate::commands::command_contract::CommandSpec for HideCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "hide",
command: "hide",
category: "admin",
params: "[salon]",
summary: "Cache un salon",
description: "Retire la visibilite d un salon.",
examples: &["+hide", "+hide #general"],
alias_source_key: "hide",
default_aliases: &["hd"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_hideall(ctx: &Context, msg: &Message) {
moderation_tools::handle_hideall_unhideall(ctx, msg, true).await;
}
pub struct HideallCommand;
pub static COMMAND_DESCRIPTOR: HideallCommand = HideallCommand;
impl crate::commands::command_contract::CommandSpec for HideallCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "hideall",
command: "hideall",
category: "admin",
params: "aucun",
summary: "Cache tous les salons",
description: "Retire la visibilite de tous les salons.",
examples: &["+hideall"],
alias_source_key: "hideall",
default_aliases: &["hda"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_idle(ctx: &Context, msg: &Message) {
botconfig_common::handle_status(ctx, msg, "+idle").await;
}
pub struct IdleCommand;
pub static COMMAND_DESCRIPTOR: IdleCommand = IdleCommand;
impl crate::commands::command_contract::CommandSpec for IdleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "idle",
command: "idle",
category: "profile",
params: "aucun",
summary: "Passe le bot en idle",
description: "Change le statut du bot en idle et sauvegarde ce statut.",
examples: &["+idle", "+ie", "+help idle"],
alias_source_key: "idle",
default_aliases: &["idl"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_invisible(ctx: &Context, msg: &Message) {
botconfig_common::handle_status(ctx, msg, "+invisible").await;
}
pub struct InvisibleCommand;
pub static COMMAND_DESCRIPTOR: InvisibleCommand = InvisibleCommand;
impl crate::commands::command_contract::CommandSpec for InvisibleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "invisible",
command: "invisible",
category: "profile",
params: "aucun",
summary: "Passe le bot en invisible",
description: "Change le statut du bot en invisible et sauvegarde ce statut.",
examples: &["+invisible", "+ie", "+help invisible"],
alias_source_key: "invisible",
default_aliases: &["ivs"],
}
}
}
+84
View File
@@ -0,0 +1,84 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::commands::server::resolve_guild_target;
pub async fn handle_invite(ctx: &Context, msg: &Message, args: &[&str]) {
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+invite <ID/nombre>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Some(guild_id) = resolve_guild_target(ctx, args[0]).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Serveur introuvable.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Ok(channels) = guild_id.channels(&ctx.http).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible de lire les salons.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let mut invite_url = None;
for channel in channels.values() {
if matches!(channel.kind, ChannelType::Text | ChannelType::News) {
if let Ok(invite) = channel
.create_invite(
&ctx.http,
serenity::builder::CreateInvite::new().max_age(3600),
)
.await
{
invite_url = Some(invite.url());
break;
}
}
}
let Some(url) = invite_url else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Aucun salon éligible pour créer une invitation.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let embed = CreateEmbed::new()
.title("Invitation créée")
.description(url)
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub struct InviteCommand;
pub static COMMAND_DESCRIPTOR: InviteCommand = InviteCommand;
impl crate::commands::command_contract::CommandSpec for InviteCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "invite",
command: "invite",
category: "admin",
params: "<ID_serveur/index>",
summary: "Cree une invitation serveur",
description: "Cree une invitation temporaire sur un serveur cible accessible par le bot.",
examples: &["+invite", "+ie", "+help invite"],
alias_source_key: "invite",
default_aliases: &["ivt"],
}
}
}
+29
View File
@@ -0,0 +1,29 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_join(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_join_leave_settings(ctx, msg, args, "join").await;
}
pub struct JoinCommand;
pub static COMMAND_DESCRIPTOR: JoinCommand = JoinCommand;
impl crate::commands::command_contract::CommandSpec for JoinCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "join",
command: "join",
category: "admin",
params: "settings [on/off] [salon] [message]",
summary: "Parametre les actions de join",
description: "Permet de configurer les actions quand un membre rejoint.",
examples: &[
"+join settings",
"+join settings on #welcome Bienvenue {user}",
],
alias_source_key: "join",
default_aliases: &["jset"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_kick(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_kick(ctx, msg, args).await;
}
pub struct KickCommand;
pub static COMMAND_DESCRIPTOR: KickCommand = KickCommand;
impl crate::commands::command_contract::CommandSpec for KickCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "kick",
command: "kick",
category: "admin",
params: "<@membre/ID[,..]> [raison]",
summary: "Expulse un membre",
description: "Kick un ou plusieurs membres.",
examples: &["+kick @User"],
alias_source_key: "kick",
default_aliases: &["k"],
}
}
}
+48
View File
@@ -0,0 +1,48 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
use crate::commands::server::resolve_guild_target;
pub async fn handle_leave(ctx: &Context, msg: &Message, args: &[&str]) {
let target = if args.is_empty() {
msg.guild_id
} else {
resolve_guild_target(ctx, args[0]).await
};
let Some(guild_id) = target else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Serveur introuvable.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let _ = guild_id.leave(&ctx.http).await;
let embed = CreateEmbed::new()
.title("Serveur quitté")
.description(format!("Le bot a quitté `{}`.", guild_id.get()))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub struct LeaveCommand;
pub static COMMAND_DESCRIPTOR: LeaveCommand = LeaveCommand;
impl crate::commands::command_contract::CommandSpec for LeaveCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "leave",
command: "leave",
category: "admin",
params: "[ID_serveur/index]",
summary: "Fait quitter un serveur",
description: "Force le bot a quitter un serveur cible ou le serveur courant.",
examples: &["+leave", "+le", "+help leave"],
alias_source_key: "leave",
default_aliases: &["lvg"],
}
}
}
+30
View File
@@ -0,0 +1,30 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::logs_service;
pub async fn handle_leave_settings(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_join_leave_settings(ctx, msg, args, "leave").await;
}
pub struct LeaveSettingsCommand;
pub static COMMAND_DESCRIPTOR: LeaveSettingsCommand = LeaveSettingsCommand;
impl crate::commands::command_contract::CommandSpec for LeaveSettingsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "leave_settings",
command: "leave settings",
category: "admin",
params: "settings [on/off] [salon] [message]",
summary: "Parametre les actions de leave",
description: "Configure les actions a executer quand un membre quitte le serveur.",
examples: &[
"+leave settings",
"+leave settings on #logs {user} a quitte",
],
alias_source_key: "leave_settings",
default_aliases: &["lset"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_listen(ctx: &Context, msg: &Message, args: &[&str]) {
botconfig_common::handle_activity(ctx, msg, "+listen", args).await;
}
pub struct ListenCommand;
pub static COMMAND_DESCRIPTOR: ListenCommand = ListenCommand;
impl crate::commands::command_contract::CommandSpec for ListenCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "listen",
command: "listen",
category: "profile",
params: "<texte[, ,texte2,...]>",
summary: "Definit une activite listening",
description: "Configure la rotation des messages d activite en mode listening.",
examples: &["+listen", "+ln", "+help listen"],
alias_source_key: "listen",
default_aliases: &["lsn"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_loading(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_loading(ctx, msg, args).await;
}
pub struct LoadingCommand;
pub static COMMAND_DESCRIPTOR: LoadingCommand = LoadingCommand;
impl crate::commands::command_contract::CommandSpec for LoadingCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "loading",
command: "loading",
category: "general",
params: "<duree> <message>",
summary: "Affiche une barre de chargement",
description: "Anime une barre de progression avec la duree et le texte fournis.",
examples: &["+loading 10s Traitement en cours"],
alias_source_key: "loading",
default_aliases: &["loadbar", "bar"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_lock(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_lock_unlock(ctx, msg, args, true).await;
}
pub struct LockCommand;
pub static COMMAND_DESCRIPTOR: LockCommand = LockCommand;
impl crate::commands::command_contract::CommandSpec for LockCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "lock",
command: "lock",
category: "admin",
params: "[salon]",
summary: "Ferme un salon",
description: "Verrouille un salon texte ou vocal.",
examples: &["+lock", "+lock #general"],
alias_source_key: "lock",
default_aliases: &["lk"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_lockall(ctx: &Context, msg: &Message) {
moderation_tools::handle_lockall_unlockall(ctx, msg, true).await;
}
pub struct LockallCommand;
pub static COMMAND_DESCRIPTOR: LockallCommand = LockallCommand;
impl crate::commands::command_contract::CommandSpec for LockallCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "lockall",
command: "lockall",
category: "admin",
params: "aucun",
summary: "Ferme tous les salons",
description: "Verrouille tous les salons du serveur.",
examples: &["+lockall"],
alias_source_key: "lockall",
default_aliases: &["lka"],
}
}
}
File diff suppressed because it is too large Load Diff
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_mainprefix(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_mainprefix(ctx, msg, args).await;
}
pub struct MainprefixCommand;
pub static COMMAND_DESCRIPTOR: MainprefixCommand = MainprefixCommand;
impl crate::commands::command_contract::CommandSpec for MainprefixCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "mainprefix",
command: "mainprefix",
category: "permissions",
params: "<prefix>",
summary: "Change le prefixe global",
description: "Definit le prefixe principal utilise par le bot sur tous les serveurs.",
examples: &["+mainprefix", "+mx", "+help mainprefix"],
alias_source_key: "mainprefix",
default_aliases: &["mpx"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_massiverole(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_massive_role(ctx, msg, args, true).await;
}
pub struct MassiveRoleCommand;
pub static COMMAND_DESCRIPTOR: MassiveRoleCommand = MassiveRoleCommand;
impl crate::commands::command_contract::CommandSpec for MassiveRoleCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "massiverole",
command: "massiverole",
category: "admin",
params: "<role_cible> [role_filtre]",
summary: "Ajoute un role en masse",
description: "Ajoute un role a tous les membres ou a ceux qui ont deja un role filtre.",
examples: &["+massiverole @VIP", "+massiverole @VIP @Membres"],
alias_source_key: "massiverole",
default_aliases: &["mrole", "mr"],
}
}
}
+91
View File
@@ -0,0 +1,91 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{discord_ts, send_embed, truncate_text};
pub async fn handle_member(ctx: &Context, msg: &Message, args: &[&str]) {
let Some(guild_id) = msg.guild_id else {
return;
};
let member = if args.is_empty() {
guild_id.member(&ctx.http, msg.author.id).await.ok()
} else {
let user_id = args[0]
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!')
.parse::<u64>()
.ok()
.map(UserId::new);
if let Some(uid) = user_id {
guild_id.member(&ctx.http, uid).await.ok()
} else {
None
}
};
let Some(member) = member else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Membre non trouvé.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let joined_at = discord_ts(
member.joined_at.unwrap_or_else(|| member.user.created_at()),
"F",
);
let created_at = discord_ts(member.user.created_at(), "F");
let avatar_url = member.user.avatar_url().unwrap_or_default();
let roles_str = if member.roles.is_empty() {
"@everyone".to_string()
} else {
let roles_list: Vec<String> = member
.roles
.iter()
.map(|r| format!("<@&{}>", r.get()))
.collect();
truncate_text(&roles_list.join(", "), 1024)
};
let mut embed = CreateEmbed::new()
.title(&member.user.name)
.description(format!("ID: `{}`", member.user.id.get()))
.color(0x5865F2)
.thumbnail(&avatar_url)
.field("Compte créé", created_at, true)
.field("A rejoint", joined_at, true)
.field("Rôles", roles_str, false);
if let Some(nick) = &member.nick {
embed = embed.field("Surnom", nick, true);
}
send_embed(ctx, msg, embed).await;
}
pub struct MemberCommand;
pub static COMMAND_DESCRIPTOR: MemberCommand = MemberCommand;
impl crate::commands::command_contract::CommandSpec for MemberCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "member",
command: "member",
category: "general",
params: "<@membre/ID>",
summary: "Affiche le profil membre",
description: "Affiche les informations dun membre dans le serveur courant.",
examples: &["+member", "+mr", "+help member"],
alias_source_key: "member",
default_aliases: &["mbr"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_messagelog(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_log_toggle(ctx, msg, args, "message", "MessageLog").await;
}
pub struct MessagelogCommand;
pub static COMMAND_DESCRIPTOR: MessagelogCommand = MessagelogCommand;
impl crate::commands::command_contract::CommandSpec for MessagelogCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "messagelog",
command: "messagelog",
category: "admin",
params: "<on [salon]|off>",
summary: "Active les logs de messages",
description: "Active ou desactive les logs des messages supprimes et edites.",
examples: &["+messagelog on #logs", "+messagelog off"],
alias_source_key: "messagelog",
default_aliases: &["msglog"],
}
}
}
+291
View File
@@ -0,0 +1,291 @@
use crate::commands::command_contract::{CommandMetadata, CommandSpec};
pub mod addrole;
pub mod admin_common;
pub mod admin_service;
pub mod advanced_tools;
pub mod alias;
pub mod alladmins;
pub mod allbots;
pub mod allperms;
pub mod autobackup;
pub mod autoconfiglog;
pub mod autoreact;
pub mod backup;
pub mod ban;
pub mod banlist;
pub mod banner;
pub mod bl;
pub mod blinfo;
pub mod boostembed;
pub mod boosters;
pub mod boostlog;
pub mod botadmins;
pub mod botconfig_common;
pub mod botconfig_service;
pub mod bringall;
pub mod button;
pub mod calc;
pub mod change;
pub mod changeall;
pub mod channel;
pub mod choose;
pub mod cleanup;
pub mod clear_all_sanctions;
pub mod clear_bl;
pub mod clear_messages;
pub mod clear_owners;
pub mod clear_perms;
pub mod clear_sanctions;
pub mod cmute;
pub mod command_contract;
pub mod common;
pub mod compet;
pub mod create;
pub mod del;
pub mod del_sanction;
pub mod delrole;
pub mod derank;
pub mod discussion;
pub mod dnd;
pub mod embed;
pub mod emoji;
pub mod end;
pub mod giveaway;
pub mod help;
pub mod helpsetting;
pub mod hide;
pub mod hideall;
pub mod idle;
pub mod invisible;
pub mod invite;
pub mod join;
pub mod kick;
pub mod leave;
pub mod leave_settings;
pub mod listen;
pub mod loading;
pub mod lock;
pub mod lockall;
pub mod logs_service;
pub mod mainprefix;
pub mod massiverole;
pub mod member;
pub mod messagelog;
pub mod moderation_tools;
pub mod modlog;
pub mod mp;
pub mod mute;
pub mod mutelist;
pub mod newsticker;
pub mod nolog;
pub mod online;
pub mod owner;
pub mod perms;
pub mod perms_service;
pub mod pic;
pub mod ping;
pub mod playto;
pub mod prefix;
pub mod raidlog;
pub mod remove_activity;
pub mod renew;
pub mod reroll;
pub mod role;
pub mod rolelog;
pub mod rolemembers;
pub mod sanctions;
pub mod say;
pub mod server;
pub mod serverinfo;
pub mod set;
pub mod set_boostembed;
pub mod set_modlogs;
pub mod shadowbot;
pub mod snipe;
pub mod stream;
pub mod sync;
pub mod tempban;
pub mod tempcmute;
pub mod tempmute;
pub mod temprole;
pub mod theme;
pub mod unban;
pub mod unbanall;
pub mod unbl;
pub mod uncmute;
pub mod unhide;
pub mod unhideall;
pub mod unlock;
pub mod unlockall;
pub mod unmassiverole;
pub mod unmute;
pub mod unmuteall;
pub mod unowner;
pub mod untemprole;
pub mod user;
pub mod viewlogs;
pub mod vocinfo;
pub mod voicekick;
pub mod ticket;
pub mod tickets;
pub mod showpics;
pub mod suggestion;
pub mod autopublish;
pub mod tempvoc;
pub mod tempvoc_cmd;
pub mod voicelog;
pub mod voicemove;
pub mod warn;
pub mod claim;
pub mod close;
pub mod rename;
pub mod ticket_member;
pub mod watch;
pub fn all_command_metadata() -> Vec<CommandMetadata> {
vec![
ping::COMMAND_DESCRIPTOR.metadata(),
allbots::COMMAND_DESCRIPTOR.metadata(),
alladmins::COMMAND_DESCRIPTOR.metadata(),
botadmins::COMMAND_DESCRIPTOR.metadata(),
boosters::COMMAND_DESCRIPTOR.metadata(),
rolemembers::COMMAND_DESCRIPTOR.metadata(),
serverinfo::COMMAND_DESCRIPTOR.metadata(),
vocinfo::COMMAND_DESCRIPTOR.metadata(),
role::COMMAND_DESCRIPTOR.metadata(),
join::COMMAND_DESCRIPTOR.metadata(),
channel::COMMAND_DESCRIPTOR.metadata(),
user::COMMAND_DESCRIPTOR.metadata(),
member::COMMAND_DESCRIPTOR.metadata(),
pic::COMMAND_DESCRIPTOR.metadata(),
banner::COMMAND_DESCRIPTOR.metadata(),
server::COMMAND_DESCRIPTOR.metadata(),
snipe::COMMAND_DESCRIPTOR.metadata(),
emoji::COMMAND_DESCRIPTOR.metadata(),
giveaway::COMMAND_DESCRIPTOR.metadata(),
modlog::COMMAND_DESCRIPTOR.metadata(),
messagelog::COMMAND_DESCRIPTOR.metadata(),
voicelog::COMMAND_DESCRIPTOR.metadata(),
boostlog::COMMAND_DESCRIPTOR.metadata(),
rolelog::COMMAND_DESCRIPTOR.metadata(),
raidlog::COMMAND_DESCRIPTOR.metadata(),
autoconfiglog::COMMAND_DESCRIPTOR.metadata(),
boostembed::COMMAND_DESCRIPTOR.metadata(),
nolog::COMMAND_DESCRIPTOR.metadata(),
set_modlogs::COMMAND_DESCRIPTOR.metadata(),
set_boostembed::COMMAND_DESCRIPTOR.metadata(),
sanctions::COMMAND_DESCRIPTOR.metadata(),
end::COMMAND_DESCRIPTOR.metadata(),
reroll::COMMAND_DESCRIPTOR.metadata(),
choose::COMMAND_DESCRIPTOR.metadata(),
embed::COMMAND_DESCRIPTOR.metadata(),
clear_messages::COMMAND_DESCRIPTOR.metadata(),
backup::COMMAND_DESCRIPTOR.metadata(),
autobackup::COMMAND_DESCRIPTOR.metadata(),
loading::COMMAND_DESCRIPTOR.metadata(),
create::COMMAND_DESCRIPTOR.metadata(),
newsticker::COMMAND_DESCRIPTOR.metadata(),
massiverole::COMMAND_DESCRIPTOR.metadata(),
unmassiverole::COMMAND_DESCRIPTOR.metadata(),
voicemove::COMMAND_DESCRIPTOR.metadata(),
voicekick::COMMAND_DESCRIPTOR.metadata(),
cleanup::COMMAND_DESCRIPTOR.metadata(),
bringall::COMMAND_DESCRIPTOR.metadata(),
renew::COMMAND_DESCRIPTOR.metadata(),
unbanall::COMMAND_DESCRIPTOR.metadata(),
warn::COMMAND_DESCRIPTOR.metadata(),
mute::COMMAND_DESCRIPTOR.metadata(),
tempmute::COMMAND_DESCRIPTOR.metadata(),
unmute::COMMAND_DESCRIPTOR.metadata(),
cmute::COMMAND_DESCRIPTOR.metadata(),
tempcmute::COMMAND_DESCRIPTOR.metadata(),
uncmute::COMMAND_DESCRIPTOR.metadata(),
mutelist::COMMAND_DESCRIPTOR.metadata(),
unmuteall::COMMAND_DESCRIPTOR.metadata(),
kick::COMMAND_DESCRIPTOR.metadata(),
ban::COMMAND_DESCRIPTOR.metadata(),
tempban::COMMAND_DESCRIPTOR.metadata(),
unban::COMMAND_DESCRIPTOR.metadata(),
banlist::COMMAND_DESCRIPTOR.metadata(),
lock::COMMAND_DESCRIPTOR.metadata(),
unlock::COMMAND_DESCRIPTOR.metadata(),
lockall::COMMAND_DESCRIPTOR.metadata(),
unlockall::COMMAND_DESCRIPTOR.metadata(),
hide::COMMAND_DESCRIPTOR.metadata(),
unhide::COMMAND_DESCRIPTOR.metadata(),
hideall::COMMAND_DESCRIPTOR.metadata(),
unhideall::COMMAND_DESCRIPTOR.metadata(),
addrole::COMMAND_DESCRIPTOR.metadata(),
delrole::COMMAND_DESCRIPTOR.metadata(),
derank::COMMAND_DESCRIPTOR.metadata(),
del_sanction::COMMAND_DESCRIPTOR.metadata(),
clear_sanctions::COMMAND_DESCRIPTOR.metadata(),
clear_all_sanctions::COMMAND_DESCRIPTOR.metadata(),
temprole::COMMAND_DESCRIPTOR.metadata(),
untemprole::COMMAND_DESCRIPTOR.metadata(),
sync::COMMAND_DESCRIPTOR.metadata(),
button::COMMAND_DESCRIPTOR.metadata(),
autoreact::COMMAND_DESCRIPTOR.metadata(),
calc::COMMAND_DESCRIPTOR.metadata(),
shadowbot::COMMAND_DESCRIPTOR.metadata(),
set::COMMAND_DESCRIPTOR.metadata(),
theme::COMMAND_DESCRIPTOR.metadata(),
playto::COMMAND_DESCRIPTOR.metadata(),
listen::COMMAND_DESCRIPTOR.metadata(),
watch::COMMAND_DESCRIPTOR.metadata(),
compet::COMMAND_DESCRIPTOR.metadata(),
stream::COMMAND_DESCRIPTOR.metadata(),
remove_activity::COMMAND_DESCRIPTOR.metadata(),
online::COMMAND_DESCRIPTOR.metadata(),
idle::COMMAND_DESCRIPTOR.metadata(),
dnd::COMMAND_DESCRIPTOR.metadata(),
invisible::COMMAND_DESCRIPTOR.metadata(),
owner::COMMAND_DESCRIPTOR.metadata(),
unowner::COMMAND_DESCRIPTOR.metadata(),
clear_owners::COMMAND_DESCRIPTOR.metadata(),
bl::COMMAND_DESCRIPTOR.metadata(),
unbl::COMMAND_DESCRIPTOR.metadata(),
blinfo::COMMAND_DESCRIPTOR.metadata(),
clear_bl::COMMAND_DESCRIPTOR.metadata(),
say::COMMAND_DESCRIPTOR.metadata(),
change::COMMAND_DESCRIPTOR.metadata(),
changeall::COMMAND_DESCRIPTOR.metadata(),
mainprefix::COMMAND_DESCRIPTOR.metadata(),
prefix::COMMAND_DESCRIPTOR.metadata(),
perms::COMMAND_DESCRIPTOR.metadata(),
del::COMMAND_DESCRIPTOR.metadata(),
clear_perms::COMMAND_DESCRIPTOR.metadata(),
allperms::COMMAND_DESCRIPTOR.metadata(),
help::COMMAND_DESCRIPTOR.metadata(),
helpsetting::COMMAND_DESCRIPTOR.metadata(),
alias::COMMAND_DESCRIPTOR.metadata(),
mp::COMMAND_DESCRIPTOR.metadata(),
invite::COMMAND_DESCRIPTOR.metadata(),
leave::COMMAND_DESCRIPTOR.metadata(),
leave_settings::COMMAND_DESCRIPTOR.metadata(),
viewlogs::COMMAND_DESCRIPTOR.metadata(),
discussion::COMMAND_DESCRIPTOR.metadata(),
]
}
pub fn command_metadata_by_key(key: &str) -> Option<CommandMetadata> {
all_command_metadata()
.into_iter()
.find(|meta| meta.key == key)
}
pub fn resolve_default_alias(alias: &str) -> Option<&'static str> {
let normalized = alias.trim().trim_start_matches('+').to_lowercase();
all_command_metadata().into_iter().find_map(|meta| {
if meta
.default_aliases
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(&normalized))
{
Some(meta.key)
} else {
None
}
})
}
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_modlog(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_log_toggle(ctx, msg, args, "moderation", "ModLog").await;
}
pub struct ModlogCommand;
pub static COMMAND_DESCRIPTOR: ModlogCommand = ModlogCommand;
impl crate::commands::command_contract::CommandSpec for ModlogCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "modlog",
command: "modlog",
category: "admin",
params: "<on [salon]|off>",
summary: "Active les logs de moderation",
description: "Active ou desactive les logs de moderation dans un salon cible.",
examples: &["+modlog on #logs", "+modlog off"],
alias_source_key: "modlog",
default_aliases: &["mlog"],
}
}
}
+474
View File
@@ -0,0 +1,474 @@
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{
add_list_fields, discord_ts, send_embed, theme_color, truncate_text,
};
use crate::db::{
DbPoolKey, count_sent_mp_messages, get_mp_enabled, get_sent_mp_message, list_sent_mp_messages,
log_sent_mp_message, mark_sent_mp_deleted, set_mp_enabled,
};
pub async fn handle_mp(ctx: &Context, msg: &Message, args: &[&str]) {
if args
.first()
.map(|value| value.eq_ignore_ascii_case("settings"))
.unwrap_or(false)
{
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.len() == 1 {
let enabled = get_mp_enabled(&pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or(true);
let embed = CreateEmbed::new()
.title("MP settings")
.description(format!(
"Envoi de MP: `{}`\nUtilise `+mp settings on/off`.",
if enabled { "on" } else { "off" }
))
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
return;
}
let enabled = match args[1].to_lowercase().as_str() {
"on" | "true" | "yes" => true,
"off" | "false" | "no" => false,
_ => {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+mp settings <on/off>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let _ = set_mp_enabled(&pool, bot_id, enabled).await;
let embed = CreateEmbed::new()
.title("MP settings mis à jour")
.description(format!(
"Envoi de MP: `{}`",
if enabled { "on" } else { "off" }
))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
return;
}
if args
.first()
.map(|value| value.eq_ignore_ascii_case("sent"))
.unwrap_or(false)
{
let page = args
.get(1)
.and_then(|value| value.parse::<i64>().ok())
.filter(|value| *value >= 1)
.unwrap_or(1);
let _ = send_mp_sent_page(ctx, msg, page).await;
return;
}
if args
.first()
.map(|value| value.eq_ignore_ascii_case("delete") || value.eq_ignore_ascii_case("del"))
.unwrap_or(false)
{
let Some(entry_id_raw) = args.get(1) else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+mp delete <id>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Ok(entry_id) = entry_id_raw.parse::<i64>() else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("ID invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Some(entry) = get_sent_mp_message(&pool, bot_id, entry_id)
.await
.ok()
.flatten()
else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Message MP introuvable.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let delete_result = ChannelId::new(entry.dm_channel_id as u64)
.delete_message(&ctx.http, MessageId::new(entry.message_id as u64))
.await;
let _ = mark_sent_mp_deleted(&pool, bot_id, entry_id).await;
if delete_result.is_err() {
let embed = CreateEmbed::new()
.title("MP déjà supprimé ou inaccessible")
.description(format!(
"Entrée `#{}` marquée supprimée en base (Discord a refusé la suppression).",
entry.entry_id
))
.color(0xFEE75C);
send_embed(ctx, msg, embed).await;
} else {
let embed = CreateEmbed::new()
.title("MP supprimé")
.description(format!("Entrée `#{}` supprimée.", entry.entry_id))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
return;
}
if args.len() < 2 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `+mp settings` ou `+mp <membre> <message>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot_id = ctx.cache.current_user().id;
let Some(db_pool) = pool(ctx).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let enabled = get_mp_enabled(&db_pool, bot_id)
.await
.ok()
.flatten()
.unwrap_or(true);
if !enabled {
let embed = CreateEmbed::new()
.title("MP désactivés")
.description("Réactive-les avec `+mp settings on`.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Some(user_id) = parse_user_id(args[0]) else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Membre invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let content = args[1..].join(" ");
let message = format!("{}\n\n- envoyé par {}", content, msg.author.tag());
let result = user_id.create_dm_channel(&ctx.http).await;
let Ok(channel) = result else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible d'ouvrir le MP.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Ok(sent_message) = channel.say(&ctx.http, message.clone()).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible d'envoyer le MP.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if let Some(pool) = pool(ctx).await {
let _ = log_sent_mp_message(
&pool,
bot_id,
msg.author.id,
user_id,
channel.id,
sent_message.id,
&message,
)
.await;
}
let embed = CreateEmbed::new()
.title("Message privé envoyé")
.description(format!("À <@{}>.", user_id.get()))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_mp_component(ctx: &Context, component: &ComponentInteraction) -> bool {
let Some((owner_id, page)) = parse_mp_sent_custom_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 la commande peut utiliser ces boutons.")
.ephemeral(true),
),
)
.await;
return true;
}
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
return true;
};
let total = count_sent_mp_messages(&pool, bot_id)
.await
.unwrap_or(0)
.max(0);
let total_pages = ((total + MP_SENT_PAGE_SIZE - 1) / MP_SENT_PAGE_SIZE).max(1);
let safe_page = page.clamp(1, total_pages);
let offset = (safe_page - 1) * MP_SENT_PAGE_SIZE;
let items = list_sent_mp_messages(&pool, bot_id, MP_SENT_PAGE_SIZE, offset)
.await
.unwrap_or_default();
let lines = items
.iter()
.map(|entry| {
let status = if entry.deleted_at.is_some() {
"supprimé"
} else {
"actif"
};
let sent_at = discord_ts(
Timestamp::from_unix_timestamp(entry.sent_at.timestamp())
.unwrap_or_else(|_| Timestamp::now()),
"F",
);
format!(
"`#{}` · de <@{}> vers <@{}> · msg `{}` · {} · {} · {}",
entry.entry_id,
entry.sender_id,
entry.recipient_id,
entry.message_id,
status,
sent_at,
truncate_text(&entry.content, 80)
)
})
.collect::<Vec<_>>();
let prev_page = if safe_page > 1 { safe_page - 1 } else { 1 };
let next_page = if safe_page < total_pages {
safe_page + 1
} else {
total_pages
};
let mut embed = CreateEmbed::new()
.title("MP envoyés")
.description(format!(
"{} message(s) · Page {}/{}",
total, safe_page, total_pages
))
.color(theme_color(ctx).await);
embed = add_list_fields(embed, &lines, "Messages");
let components = vec![CreateActionRow::Buttons(vec![
CreateButton::new(format!("mpsent:{}:{}", component.user.id.get(), prev_page))
.label("◀ Précédent")
.style(ButtonStyle::Primary)
.disabled(safe_page <= 1),
CreateButton::new(format!("mpsent:{}:{}", component.user.id.get(), next_page))
.label("Suivant ▶")
.style(ButtonStyle::Primary)
.disabled(safe_page >= total_pages),
])];
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(embed)
.components(components),
),
)
.await;
true
}
const MP_SENT_PAGE_SIZE: i64 = 10;
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
}
async fn send_mp_sent_page(ctx: &Context, msg: &Message, page: i64) -> Result<(), ()> {
let bot_id = ctx.cache.current_user().id;
let Some(pool) = pool(ctx).await else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return Err(());
};
let total = count_sent_mp_messages(&pool, bot_id)
.await
.unwrap_or(0)
.max(0);
let total_pages = ((total + MP_SENT_PAGE_SIZE - 1) / MP_SENT_PAGE_SIZE).max(1);
let safe_page = page.clamp(1, total_pages) as i64;
let offset = (safe_page - 1) * MP_SENT_PAGE_SIZE;
let items = list_sent_mp_messages(&pool, bot_id, MP_SENT_PAGE_SIZE, offset)
.await
.unwrap_or_default();
let lines = items
.iter()
.map(|entry| {
let status = if entry.deleted_at.is_some() {
"supprimé"
} else {
"actif"
};
let sent_at = discord_ts(
Timestamp::from_unix_timestamp(entry.sent_at.timestamp())
.unwrap_or_else(|_| Timestamp::now()),
"F",
);
format!(
"`#{}` · de <@{}> vers <@{}> · msg `{}` · {} · {} · {}",
entry.entry_id,
entry.sender_id,
entry.recipient_id,
entry.message_id,
status,
sent_at,
truncate_text(&entry.content, 80)
)
})
.collect::<Vec<_>>();
let prev_page = if safe_page > 1 { safe_page - 1 } else { 1 };
let next_page = if safe_page < total_pages {
safe_page + 1
} else {
total_pages
};
let mut embed = CreateEmbed::new()
.title("MP envoyés")
.description(format!(
"{} message(s) · Page {}/{}",
total, safe_page, total_pages
))
.color(theme_color(ctx).await);
embed = add_list_fields(embed, &lines, "Messages");
let components = vec![CreateActionRow::Buttons(vec![
CreateButton::new(format!("mpsent:{}:{}", msg.author.id.get(), prev_page))
.label("◀ Précédent")
.style(ButtonStyle::Primary)
.disabled(safe_page <= 1),
CreateButton::new(format!("mpsent:{}:{}", msg.author.id.get(), next_page))
.label("Suivant ▶")
.style(ButtonStyle::Primary)
.disabled(safe_page >= total_pages),
])];
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(embed).components(components),
)
.await;
Ok(())
}
fn parse_mp_sent_custom_id(custom_id: &str) -> Option<(u64, i64)> {
let parts = custom_id.split(':').collect::<Vec<_>>();
if parts.len() != 3 || parts.first().copied()? != "mpsent" {
return None;
}
let owner_id = parts.get(1)?.parse::<u64>().ok()?;
let page = parts.get(2)?.parse::<i64>().ok()?;
Some((owner_id, page))
}
fn parse_user_id(input: &str) -> Option<UserId> {
let cleaned = input
.trim()
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!');
cleaned.parse::<u64>().ok().map(UserId::new)
}
pub struct MpCommand;
pub static COMMAND_DESCRIPTOR: MpCommand = MpCommand;
impl crate::commands::command_contract::CommandSpec for MpCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "mp",
command: "mp",
category: "profile",
params: "settings [on|off] | sent [page] | delete <id> | <@membre/ID> <message...>",
summary: "Gere lenvoi de messages prives",
description: "Permet de configurer, envoyer, lister et supprimer des messages prives envoyes.",
examples: &["+mp", "+help mp"],
alias_source_key: "mp",
default_aliases: &["dmsg"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_mute(ctx: &Context, msg: &Message, args: &[&str]) {
moderation_tools::handle_mute(ctx, msg, args, false).await;
}
pub struct MuteCommand;
pub static COMMAND_DESCRIPTOR: MuteCommand = MuteCommand;
impl crate::commands::command_contract::CommandSpec for MuteCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "mute",
command: "mute",
category: "admin",
params: "<@membre/ID[,..]> [raison]",
summary: "Mute un membre",
description: "Applique un mute a un ou plusieurs membres.",
examples: &["+mute @User abus"],
alias_source_key: "mute",
default_aliases: &["tmute"],
}
}
}
+23
View File
@@ -0,0 +1,23 @@
use crate::commands::moderation_tools;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_mutelist(ctx: &Context, msg: &Message) {
moderation_tools::handle_mutelist(ctx, msg).await;
}
pub struct MutelistCommand;
pub static COMMAND_DESCRIPTOR: MutelistCommand = MutelistCommand;
impl crate::commands::command_contract::CommandSpec for MutelistCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "mutelist",
command: "mutelist",
category: "admin",
params: "aucun",
summary: "Liste les mutes",
description: "Affiche tous les mutes en cours.",
examples: &["+mutelist"],
alias_source_key: "mutelist",
default_aliases: &["ml"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::advanced_tools;
pub async fn handle_newsticker(ctx: &Context, msg: &Message, args: &[&str]) {
advanced_tools::handle_new_sticker(ctx, msg, args).await;
}
pub struct NewStickerCommand;
pub static COMMAND_DESCRIPTOR: NewStickerCommand = NewStickerCommand;
impl crate::commands::command_contract::CommandSpec for NewStickerCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "newsticker",
command: "newsticker",
category: "admin",
params: "[nom]",
summary: "Cree un sticker serveur",
description: "Cree un nouveau sticker a partir d'un sticker ou fichier repondu.",
examples: &["+newsticker cool_pack"],
alias_source_key: "newsticker",
default_aliases: &["stcreate", "nst"],
}
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::commands::logs_service;
use serenity::model::prelude::*;
use serenity::prelude::*;
pub async fn handle_nolog(ctx: &Context, msg: &Message, args: &[&str]) {
logs_service::handle_nolog(ctx, msg, args).await;
}
pub struct NologCommand;
pub static COMMAND_DESCRIPTOR: NologCommand = NologCommand;
impl crate::commands::command_contract::CommandSpec for NologCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "nolog",
command: "nolog",
category: "admin",
params: "<add/del> [salon] [message|voice|all]",
summary: "Exclut des salons des logs",
description: "Desactive ou reactive les logs message/voice pour certains salons.",
examples: &["+nolog add #secret all", "+nolog del #secret message"],
alias_source_key: "nolog",
default_aliases: &["nlg"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_online(ctx: &Context, msg: &Message) {
botconfig_common::handle_status(ctx, msg, "+online").await;
}
pub struct OnlineCommand;
pub static COMMAND_DESCRIPTOR: OnlineCommand = OnlineCommand;
impl crate::commands::command_contract::CommandSpec for OnlineCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "online",
command: "online",
category: "profile",
params: "aucun",
summary: "Passe le bot en online",
description: "Change le statut du bot en online et sauvegarde ce statut.",
examples: &["+online", "+oe", "+help online"],
alias_source_key: "online",
default_aliases: &["onl"],
}
}
}
+59
View File
@@ -0,0 +1,59 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::admin_common::{app_owner_id, ensure_owner};
use crate::commands::common::{add_list_fields, send_embed, theme_color};
use crate::db::{DbPoolKey, list_bot_owners};
pub async fn handle_owner(ctx: &Context, msg: &Message, _args: &[&str]) {
if ensure_owner(ctx, msg).await.is_err() {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let mut lines: Vec<String> = Vec::new();
if let Some(app_owner) = app_owner_id(ctx).await {
lines.push(format!("<@{}> (owner application)", app_owner.get()));
}
if let Some(pool) = pool {
if let Ok(extra) = list_bot_owners(&pool, bot_id).await {
for uid in extra {
lines.push(format!("<@{}>", uid));
}
}
}
let color = theme_color(ctx).await;
let mut embed = serenity::builder::CreateEmbed::new()
.title("Owners du bot")
.color(color);
embed = add_list_fields(embed, &lines, "Owners");
send_embed(ctx, msg, embed).await;
}
pub struct OwnerCommand;
pub static COMMAND_DESCRIPTOR: OwnerCommand = OwnerCommand;
impl crate::commands::command_contract::CommandSpec for OwnerCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "owner",
command: "owner",
category: "admin",
params: "aucun",
summary: "Liste les owners du bot",
description: "Affiche l owner application et les owners ajoutes en base.",
examples: &["+owner", "+or", "+help owner"],
alias_source_key: "owner",
default_aliases: &["own"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::perms_service;
pub async fn handle_perms(ctx: &Context, msg: &Message, args: &[&str]) {
perms_service::handle_perms(ctx, msg, args).await;
}
pub struct PermsCommand;
pub static COMMAND_DESCRIPTOR: PermsCommand = PermsCommand;
impl crate::commands::command_contract::CommandSpec for PermsCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "perms",
command: "perms",
category: "permissions",
params: "aucun",
summary: "Affiche les permissions ACL",
description: "Affiche les permissions ACL configurees par role ou scope.",
examples: &["+perms", "+ps", "+help perms"],
alias_source_key: "perms",
default_aliases: &["prm"],
}
}
}
+667
View File
@@ -0,0 +1,667 @@
use serenity::builder::{
CreateActionRow, CreateButton, CreateEmbed, CreateInteractionResponse,
CreateInteractionResponseMessage, CreateMessage,
};
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::{add_list_fields, send_embed, theme_color, truncate_text};
use crate::db::{
DbPoolKey, clear_role_permissions, grant_command_access, grant_perm_level,
list_role_command_access, list_role_perm_levels, list_role_scopes, remove_scope_permissions,
reset_command_permissions, set_command_permission, set_guild_prefix, set_main_prefix,
};
use crate::permissions::{
all_command_keys, command_required_permission, default_permission, is_owner_user,
};
const ALLPERMS_PAGE_SIZE: usize = 12;
const ALLPERMS_CUSTOM_ID_PREFIX: &str = "allperms";
fn parse_user_or_role(input: &str) -> Option<(&'static str, u64)> {
let trimmed = input.trim();
if trimmed.starts_with("<@&") && trimmed.ends_with('>') {
return trimmed
.trim_start_matches("<@&")
.trim_end_matches('>')
.parse::<u64>()
.ok()
.map(|id| ("role", id));
}
if (trimmed.starts_with("<@") && trimmed.ends_with('>')) || trimmed.parse::<u64>().is_ok() {
let cleaned = trimmed
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!');
if let Ok(id) = cleaned.parse::<u64>() {
return Some(("user", id));
}
}
None
}
fn normalize_command_name(input: &str) -> String {
input
.trim_start_matches('+')
.replace(' ', "_")
.to_lowercase()
}
async fn ensure_owner(ctx: &Context, msg: &Message) -> bool {
if is_owner_user(ctx, msg.author.id).await {
true
} else {
let embed = CreateEmbed::new()
.title("Accès refusé")
.description("Commande réservée aux owners.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
false
}
}
pub async fn handle_change(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args
.first()
.map(|s| s.eq_ignore_ascii_case("reset"))
.unwrap_or(false)
{
let removed = reset_command_permissions(&pool, bot_id).await.unwrap_or(0);
let embed = CreateEmbed::new()
.title("Permissions réinitialisées")
.description(format!("Overrides supprimés: {}", removed))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
return;
}
if args.len() < 2 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `change <commande> <permission>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let command = normalize_command_name(args[0]);
let Ok(level) = args[1].parse::<u8>() else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permission invalide (0..9).`).")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if level > 9 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permission invalide (0..9).`).")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let _ = set_command_permission(&pool, bot_id, &command, level).await;
let embed = CreateEmbed::new()
.title("Permission modifiée")
.description(format!("`{}` -> niveau `{}`", command, level))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_changeall(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
if args.len() < 2 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `changeall <permission> <permission>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let Ok(from) = args[0].parse::<u8>() else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permission source invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let Ok(to) = args[1].parse::<u8>() else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permission cible invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if from > 9 || to > 9 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permissions valides: 0..9")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let mut updated = 0usize;
for cmd in all_command_keys() {
let current = command_required_permission(ctx, &cmd).await;
if current == from {
let _ = set_command_permission(&pool, bot_id, &cmd, to).await;
updated += 1;
}
}
let embed = CreateEmbed::new()
.title("Changeall appliqué")
.description(format!("{} commande(s): {} -> {}", updated, from, to))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_mainprefix(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `mainprefix <préfixe>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let prefix = args[0].trim();
if prefix.is_empty() || prefix.len() > 5 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Préfixe invalide (1 à 5 caractères).`).")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let _ = set_main_prefix(&pool, bot_id, prefix).await;
}
let embed = CreateEmbed::new()
.title("Préfixe principal mis à jour")
.description(format!("Nouveau préfixe principal: `{}`", prefix))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_prefix(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
let Some(guild_id) = msg.guild_id else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Commande disponible uniquement sur un serveur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if args.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `prefix <préfixe>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let prefix = args[0].trim();
if prefix.is_empty() || prefix.len() > 5 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Préfixe invalide (1 à 5 caractères).`).")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
if let Some(pool) = pool {
let _ = set_guild_prefix(&pool, bot_id, guild_id, prefix).await;
}
let embed = CreateEmbed::new()
.title("Préfixe serveur mis à jour")
.description(format!("Nouveau préfixe ici: `{}`", prefix))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_set_perm(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
if args.len() < 3 || !args[0].eq_ignore_ascii_case("perm") {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `set perm <permission/commande> <rôle/membre>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let target = parse_user_or_role(args[2]);
let Some((scope_type, scope_id)) = target else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Rôle/membre invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
if let Ok(level) = args[1].parse::<u8>() {
if level > 9 {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Permission invalide (0..9).`).")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let _ = grant_perm_level(&pool, bot_id, scope_type, scope_id, level).await;
let who = if scope_type == "role" {
format!("<@&{}>", scope_id)
} else {
format!("<@{}>", scope_id)
};
let embed = CreateEmbed::new()
.title("Permission attribuée")
.description(format!("{} reçoit la permission `{}`", who, level))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
return;
}
let command = normalize_command_name(args[1]);
let _ = grant_command_access(&pool, bot_id, scope_type, scope_id, &command).await;
let who = if scope_type == "role" {
format!("<@&{}>", scope_id)
} else {
format!("<@{}>", scope_id)
};
let embed = CreateEmbed::new()
.title("Accès commande attribué")
.description(format!("{} reçoit l'accès direct à `{}`", who, command))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_del_perm(ctx: &Context, msg: &Message, args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
if args.len() < 2 || !args[0].eq_ignore_ascii_case("perm") {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Usage: `del perm <rôle>`")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let target = parse_user_or_role(args[1]);
let Some((scope_type, scope_id)) = target else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Rôle/membre invalide.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let removed = remove_scope_permissions(&pool, bot_id, scope_type, scope_id)
.await
.unwrap_or(0);
let embed = CreateEmbed::new()
.title("Permissions supprimées")
.description(format!("{} entrée(s) supprimée(s).", removed))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_clear_perms(ctx: &Context, msg: &Message) {
if !ensure_owner(ctx, msg).await {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let removed = clear_role_permissions(&pool, bot_id).await.unwrap_or(0);
let embed = CreateEmbed::new()
.title("Permissions rôles supprimées")
.description(format!("{} entrée(s) supprimée(s).", removed))
.color(0x57F287);
send_embed(ctx, msg, embed).await;
}
pub async fn handle_perms(ctx: &Context, msg: &Message, _args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
let bot_id = ctx.cache.current_user().id;
let pool = {
let data = ctx.data.read().await;
data.get::<DbPoolKey>().cloned()
};
let Some(pool) = pool else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("DB indisponible.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
};
let roles = list_role_scopes(&pool, bot_id).await.unwrap_or_default();
let mut lines = Vec::new();
for rid in roles {
let perm_levels = list_role_perm_levels(&pool, bot_id, rid as u64)
.await
.unwrap_or_default();
let command_access = list_role_command_access(&pool, bot_id, rid as u64)
.await
.unwrap_or_default();
let perms = if perm_levels.is_empty() {
"aucun".to_string()
} else {
perm_levels
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(",")
};
let commands = if command_access.is_empty() {
"aucune".to_string()
} else {
truncate_text(&command_access.join(", "), 80)
};
lines.push(format!(
"<@&{}> · perms [{}] · cmd [{}]",
rid, perms, commands
));
}
let mut embed = CreateEmbed::new().title("Permissions du bot");
embed = add_list_fields(embed, &lines, "Rôles configurés");
send_embed(ctx, msg, embed).await;
}
pub async fn handle_allperms(ctx: &Context, msg: &Message, _args: &[&str]) {
if !ensure_owner(ctx, msg).await {
return;
}
let lines = collect_allperms_lines(ctx).await;
let total_pages = total_pages_for(lines.len());
let requested_page = _args
.first()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(1)
.saturating_sub(1);
let page = requested_page.min(total_pages.saturating_sub(1));
let color = theme_color(ctx).await;
let embed = build_allperms_embed(&lines, page, color);
let components = allperms_components(msg.author.id, page, total_pages);
let _ = msg
.channel_id
.send_message(
&ctx.http,
CreateMessage::new().embed(embed).components(components),
)
.await;
}
pub async fn handle_allperms_component(ctx: &Context, component: &ComponentInteraction) -> bool {
let Some((owner_id, requested_page)) = parse_allperms_custom_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 la commande peut utiliser ces boutons.")
.ephemeral(true),
),
)
.await;
return true;
}
let lines = collect_allperms_lines(ctx).await;
let total_pages = total_pages_for(lines.len());
let page = requested_page.min(total_pages.saturating_sub(1));
let color = theme_color(ctx).await;
let embed = build_allperms_embed(&lines, page, color);
let components = allperms_components(component.user.id, page, total_pages);
let _ = component
.create_response(
&ctx.http,
CreateInteractionResponse::UpdateMessage(
CreateInteractionResponseMessage::new()
.embed(embed)
.components(components),
),
)
.await;
true
}
async fn collect_allperms_lines(ctx: &Context) -> Vec<String> {
let mut commands = all_command_keys();
if !commands.iter().any(|c| c == "allperms") {
commands.push("allperms".to_string());
}
commands.sort();
let mut lines = Vec::with_capacity(commands.len());
for cmd in commands {
let required = command_required_permission(ctx, &cmd).await;
let default = default_permission(&cmd);
if required == default {
lines.push(format!("`{}` -> `{}`", cmd, required));
} else {
lines.push(format!(
"`{}` -> `{}` (défaut `{}`)",
cmd, required, default
));
}
}
lines
}
fn total_pages_for(total_items: usize) -> usize {
((total_items + ALLPERMS_PAGE_SIZE.saturating_sub(1)) / ALLPERMS_PAGE_SIZE).max(1)
}
fn build_allperms_embed(lines: &[String], page: usize, color: u32) -> CreateEmbed {
let total_pages = total_pages_for(lines.len());
let safe_page = page.min(total_pages.saturating_sub(1));
let start = safe_page * ALLPERMS_PAGE_SIZE;
let end = (start + ALLPERMS_PAGE_SIZE).min(lines.len());
let chunk = if start < end { &lines[start..end] } else { &[] };
let value = if chunk.is_empty() {
"Aucune commande.".to_string()
} else {
truncate_text(&chunk.join("\n"), 1024)
};
CreateEmbed::new()
.title("Permissions de toutes les commandes")
.description(format!(
"{} commande(s) · Page {}/{}",
lines.len(),
safe_page + 1,
total_pages
))
.field("Niveaux requis", value, false)
.color(color)
}
fn allperms_components(owner_id: UserId, page: usize, total_pages: usize) -> Vec<CreateActionRow> {
let safe_total = total_pages.max(1);
let safe_page = page.min(safe_total.saturating_sub(1));
let prev_page = safe_page.saturating_sub(1);
let next_page = (safe_page + 1).min(safe_total.saturating_sub(1));
vec![CreateActionRow::Buttons(vec![
CreateButton::new(format!(
"{}:{}:{}",
ALLPERMS_CUSTOM_ID_PREFIX,
owner_id.get(),
prev_page
))
.label("◀ Précédent")
.style(ButtonStyle::Primary)
.disabled(safe_page == 0),
CreateButton::new(format!(
"{}:{}:{}",
ALLPERMS_CUSTOM_ID_PREFIX,
owner_id.get(),
next_page
))
.label("Suivant ▶")
.style(ButtonStyle::Primary)
.disabled(safe_page + 1 >= safe_total),
])]
}
fn parse_allperms_custom_id(custom_id: &str) -> Option<(u64, usize)> {
let mut parts = custom_id.split(':');
let prefix = parts.next()?;
if prefix != ALLPERMS_CUSTOM_ID_PREFIX {
return None;
}
let owner_id = parts.next()?.parse::<u64>().ok()?;
let page = parts.next()?.parse::<usize>().ok()?;
Some((owner_id, page))
}
+78
View File
@@ -0,0 +1,78 @@
use serenity::builder::CreateEmbed;
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::common::send_embed;
pub async fn handle_pic(ctx: &Context, msg: &Message, args: &[&str]) {
let user = if args.is_empty() {
msg.author.clone()
} else {
let user_id = args[0]
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('@')
.trim_start_matches('!')
.parse::<u64>()
.ok()
.map(UserId::new);
if let Some(uid) = user_id {
match ctx.http.get_user(uid).await {
Ok(u) => u,
Err(_) => {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Utilisateur non trouvé.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
}
} else {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Impossible de parser l'utilisateur.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
};
let avatar_url = user.avatar_url().unwrap_or_default();
if avatar_url.is_empty() {
let embed = CreateEmbed::new()
.title("Erreur")
.description("Cet utilisateur n'a pas de photo de profil.")
.color(0xED4245);
send_embed(ctx, msg, embed).await;
return;
}
let embed = CreateEmbed::new()
.title(format!("Photo de profil de {}", user.name))
.image(avatar_url)
.color(0x5865F2);
send_embed(ctx, msg, embed).await;
}
pub struct PicCommand;
pub static COMMAND_DESCRIPTOR: PicCommand = PicCommand;
impl crate::commands::command_contract::CommandSpec for PicCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "pic",
command: "pic",
category: "general",
params: "<@membre/ID>",
summary: "Affiche la photo de profil",
description: "Affiche la photo de profil dun utilisateur cible ou de lauteur.",
examples: &["+pic", "+pc", "+help pic"],
alias_source_key: "pic",
default_aliases: &["pfp"],
}
}
}
+64
View File
@@ -0,0 +1,64 @@
use serenity::builder::{CreateEmbed, CreateMessage, EditMessage};
use serenity::model::prelude::*;
use serenity::prelude::*;
use std::time::Instant;
use crate::commands::common::{has_flag, theme_color};
pub async fn handle_ping(ctx: &Context, msg: &Message, args: &[&str]) {
let detailed = has_flag(args, &["--details", "-d", "full"]);
let color = theme_color(ctx).await;
let start = Instant::now();
let pending_embed = CreateEmbed::new()
.title("Pong")
.description("Mesure de la latence en cours...")
.color(color);
let sent = msg
.channel_id
.send_message(&ctx.http, CreateMessage::new().embed(pending_embed))
.await;
let Ok(mut sent_message) = sent else {
return;
};
let latency_ms = start.elapsed().as_millis();
let mut embed = CreateEmbed::new()
.title("Pong")
.description("Le bot répond correctement.")
.color(color)
.field("Latence", format!("{} ms", latency_ms), true);
if detailed {
embed = embed.field("Canal", format!("<#{}>", msg.channel_id.get()), true);
if let Some(guild_id) = msg.guild_id {
embed = embed.field("Serveur", guild_id.to_string(), true);
}
}
let _ = sent_message
.edit(&ctx.http, EditMessage::new().embed(embed))
.await;
}
pub struct PingCommand;
pub static COMMAND_DESCRIPTOR: PingCommand = PingCommand;
impl crate::commands::command_contract::CommandSpec for PingCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "ping",
command: "ping",
category: "general",
params: "aucun",
summary: "Mesure la latence du bot",
description: "Affiche le temps de reponse du bot et met a jour un embed avec la latence calculee.",
examples: &["+ping", "+pg", "+help ping"],
alias_source_key: "ping",
default_aliases: &["pg"],
}
}
}
+27
View File
@@ -0,0 +1,27 @@
use serenity::model::prelude::*;
use serenity::prelude::*;
use crate::commands::botconfig_common;
pub async fn handle_playto(ctx: &Context, msg: &Message, args: &[&str]) {
botconfig_common::handle_activity(ctx, msg, "+playto", args).await;
}
pub struct PlaytoCommand;
pub static COMMAND_DESCRIPTOR: PlaytoCommand = PlaytoCommand;
impl crate::commands::command_contract::CommandSpec for PlaytoCommand {
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
crate::commands::command_contract::CommandMetadata {
key: "playto",
command: "playto",
category: "profile",
params: "<texte[, ,texte2,...]>",
summary: "Definit une activite playing",
description: "Configure la rotation des messages d activite en mode playing.",
examples: &["+playto", "+po", "+help playto"],
alias_source_key: "playto",
default_aliases: &["ply"],
}
}
}

Some files were not shown because too many files have changed in this diff Show More