mirror of
https://github.com/arthur-pbty/shadowbot.git
synced 2026-06-09 01:02:56 +02:00
chore(commands): reorganize command files by metadata categories
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
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 {
|
||||
name: "calc",
|
||||
category: "fun",
|
||||
params: "<expression>",
|
||||
description: "Evalue une expression numerique simple et renvoie le resultat.",
|
||||
examples: &["+calc", "+cc", "+help calc"],
|
||||
default_aliases: &["clc"],
|
||||
allow_in_dm: true,
|
||||
default_permission: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
use rand::seq::SliceRandom;
|
||||
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage};
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::commands::common::{send_embed, theme_color};
|
||||
|
||||
pub async fn handle_choose(ctx: &Context, msg: &Message, args: &[&str]) {
|
||||
if args.is_empty() {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Choose")
|
||||
.description("Ouvre un modal pour saisir les options (séparées par `|`).")
|
||||
.color(theme_color(ctx).await);
|
||||
let components = vec![CreateActionRow::Buttons(vec![
|
||||
CreateButton::new(format!("adv:choose:modal:{}", msg.author.id.get()))
|
||||
.label("Saisir les options")
|
||||
.style(serenity::all::ButtonStyle::Primary),
|
||||
])];
|
||||
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
CreateMessage::new().embed(embed).components(components),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let merged = args.join(" ");
|
||||
let mut options = merged
|
||||
.split('|')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if options.len() < 2 {
|
||||
options = args.iter().map(|s| (*s).to_string()).collect();
|
||||
}
|
||||
|
||||
if options.len() < 2 {
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Choose")
|
||||
.description("Donne au moins 2 options.")
|
||||
.color(0xED4245),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let pick = options
|
||||
.choose(&mut rand::thread_rng())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| options[0].clone());
|
||||
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Tirage")
|
||||
.description(format!("Résultat: **{}**", pick))
|
||||
.color(theme_color(ctx).await),
|
||||
)
|
||||
.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 {
|
||||
name: "choose",
|
||||
category: "fun",
|
||||
params: "<option1 | option2 | ...>",
|
||||
description: "Lance un tirage au sort instantane parmi les options donnees.",
|
||||
examples: &["+choose rouge | bleu | vert"],
|
||||
default_aliases: &["pick", "random"],
|
||||
allow_in_dm: false,
|
||||
default_permission: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage};
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::commands::common::{send_embed, theme_color};
|
||||
|
||||
fn owned_component_id(action: &str, owner_id: UserId) -> String {
|
||||
format!("{}:{}", action, owner_id.get())
|
||||
}
|
||||
|
||||
pub async fn handle_embed(ctx: &Context, msg: &Message, args: &[&str]) {
|
||||
if args.is_empty() {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Embed")
|
||||
.description("Utilise le bouton pour ouvrir le generateur d'embed.")
|
||||
.color(theme_color(ctx).await);
|
||||
let components = vec![CreateActionRow::Buttons(vec![
|
||||
CreateButton::new(owned_component_id("adv:embed:modal", msg.author.id))
|
||||
.label("Ouvrir le generateur")
|
||||
.style(ButtonStyle::Primary),
|
||||
])];
|
||||
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
CreateMessage::new().embed(embed).components(components),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let joined = args.join(" ");
|
||||
let mut split = joined.splitn(2, '|').map(str::trim);
|
||||
let title = split.next().unwrap_or_default();
|
||||
let description = split.next().unwrap_or_default();
|
||||
|
||||
if title.is_empty() || description.is_empty() {
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Embed")
|
||||
.description("Format attendu: +embed titre | description")
|
||||
.color(0xED4245),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
CreateMessage::new().embed(
|
||||
CreateEmbed::new()
|
||||
.title(title)
|
||||
.description(description)
|
||||
.color(theme_color(ctx).await),
|
||||
),
|
||||
)
|
||||
.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 {
|
||||
name: "embed",
|
||||
category: "fun",
|
||||
params: "title | description (v1)",
|
||||
description: "Affiche un generateur d'embed interactif version rapide.",
|
||||
examples: &["+embed"],
|
||||
default_aliases: &["emb"],
|
||||
allow_in_dm: false,
|
||||
default_permission: 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
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 {
|
||||
name: "emoji",
|
||||
category: "fun",
|
||||
params: "<emoji>",
|
||||
description: "Affiche les details dun emoji fourni.",
|
||||
examples: &["+emoji", "+ei", "+help emoji"],
|
||||
default_aliases: &["emj"],
|
||||
allow_in_dm: true,
|
||||
default_permission: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
use serenity::builder::{CreateActionRow, CreateButton, CreateEmbed, CreateMessage, EditMessage};
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::commands::common::theme_color;
|
||||
|
||||
fn duration_from_input(input: &str) -> Option<Duration> {
|
||||
let raw = input.trim().to_lowercase();
|
||||
if raw.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut number = String::new();
|
||||
let mut suffix = String::new();
|
||||
for ch in raw.chars() {
|
||||
if ch.is_ascii_digit() {
|
||||
if !suffix.is_empty() {
|
||||
return None;
|
||||
}
|
||||
number.push(ch);
|
||||
} else if !ch.is_whitespace() {
|
||||
suffix.push(ch);
|
||||
}
|
||||
}
|
||||
|
||||
let value = number.parse::<u64>().ok()?;
|
||||
let secs = match suffix.as_str() {
|
||||
"s" | "sec" | "secs" | "seconde" | "secondes" => value,
|
||||
"m" | "min" | "mins" | "minute" | "minutes" => value * 60,
|
||||
"h" | "heure" | "heures" => value * 3600,
|
||||
"j" | "d" | "jour" | "jours" => value * 86400,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Duration::from_secs(secs.max(1)))
|
||||
}
|
||||
|
||||
pub async fn handle_loading(ctx: &Context, msg: &Message, args: &[&str]) {
|
||||
if args.len() < 2 {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Loading")
|
||||
.description("Ouvre un modal pour saisir la durée et le message.")
|
||||
.color(theme_color(ctx).await);
|
||||
|
||||
let components = vec![CreateActionRow::Buttons(vec![
|
||||
CreateButton::new(format!("adv:loading:modal:{}", msg.author.id.get()))
|
||||
.label("Configurer")
|
||||
.style(serenity::all::ButtonStyle::Primary),
|
||||
])];
|
||||
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
CreateMessage::new().embed(embed).components(components),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(duration) = duration_from_input(args[0]) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let total_secs = duration.as_secs().clamp(1, 120);
|
||||
let text = args[1..].join(" ");
|
||||
|
||||
let mut sent = match msg
|
||||
.channel_id
|
||||
.send_message(&ctx.http, CreateMessage::new().content("[----------] 0%"))
|
||||
.await
|
||||
{
|
||||
Ok(m) => m,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
for i in 0..=10_u64 {
|
||||
let done = "#".repeat(i as usize);
|
||||
let todo = "-".repeat((10 - i) as usize);
|
||||
let percent = i * 10;
|
||||
|
||||
let _ = sent
|
||||
.edit(
|
||||
&ctx.http,
|
||||
EditMessage::new().content(format!("{} [{}{}] {}%", text, done, todo, percent)),
|
||||
)
|
||||
.await;
|
||||
|
||||
if i < 10 {
|
||||
tokio::time::sleep(Duration::from_secs((total_secs / 10).max(1))).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 {
|
||||
name: "loading",
|
||||
category: "fun",
|
||||
params: "<duree> <message>",
|
||||
description: "Anime une barre de progression avec la duree et le texte fournis.",
|
||||
examples: &["+loading 10s Traitement en cours"],
|
||||
default_aliases: &["loadbar", "bar"],
|
||||
allow_in_dm: false,
|
||||
default_permission: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::commands::admin_common::ensure_owner;
|
||||
|
||||
pub async fn handle_say(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: `+say <message>`")
|
||||
.color(0xED4245);
|
||||
crate::commands::common::send_embed(ctx, msg, embed).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let text = args.join(" ");
|
||||
let _ = msg.channel_id.say(&ctx.http, text).await;
|
||||
}
|
||||
|
||||
pub struct SayCommand;
|
||||
pub static COMMAND_DESCRIPTOR: SayCommand = SayCommand;
|
||||
|
||||
impl crate::commands::command_contract::CommandSpec for SayCommand {
|
||||
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
|
||||
crate::commands::command_contract::CommandMetadata {
|
||||
name: "say",
|
||||
category: "fun",
|
||||
params: "<message...>",
|
||||
description: "Envoie un message brut dans le salon courant via le bot.",
|
||||
examples: &["+say", "+sy", "+help say"],
|
||||
default_aliases: &["sym"],
|
||||
allow_in_dm: false,
|
||||
default_permission: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
use serenity::builder::CreateEmbed;
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::commands::common::{discord_ts, send_embed, truncate_text};
|
||||
use crate::db::{DbPoolKey, get_last_deleted_in_channel};
|
||||
|
||||
pub async fn handle_snipe(ctx: &Context, msg: &Message, _args: &[&str]) {
|
||||
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("Base de données indisponible.")
|
||||
.color(0xED4245);
|
||||
send_embed(ctx, msg, embed).await;
|
||||
return;
|
||||
};
|
||||
|
||||
let result = get_last_deleted_in_channel(&pool, bot_id, msg.channel_id).await;
|
||||
|
||||
let Ok(sniped) = result else {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Erreur")
|
||||
.description("Impossible de lire le snipe depuis la base de données.")
|
||||
.color(0xED4245);
|
||||
send_embed(ctx, msg, embed).await;
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(sniped) = sniped else {
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Snipe")
|
||||
.description("Aucun message supprimé enregistré dans ce salon.")
|
||||
.color(0x5865F2);
|
||||
send_embed(ctx, msg, embed).await;
|
||||
return;
|
||||
};
|
||||
|
||||
let author = sniped
|
||||
.author_id
|
||||
.map(|id| format!("<@{}>", id))
|
||||
.unwrap_or_else(|| "Inconnu".to_string());
|
||||
|
||||
let deleted_at = discord_ts(
|
||||
Timestamp::from_unix_timestamp(sniped.deleted_at.timestamp())
|
||||
.unwrap_or_else(|_| Timestamp::now()),
|
||||
"F",
|
||||
);
|
||||
|
||||
let embed = CreateEmbed::new()
|
||||
.title("Dernier message supprimé")
|
||||
.color(0xFEE75C)
|
||||
.field("Auteur", author, true)
|
||||
.field("Supprimé", deleted_at, true)
|
||||
.field("Contenu", truncate_text(&sniped.content, 1024), false);
|
||||
|
||||
send_embed(ctx, msg, embed).await;
|
||||
}
|
||||
|
||||
pub struct SnipeCommand;
|
||||
pub static COMMAND_DESCRIPTOR: SnipeCommand = SnipeCommand;
|
||||
|
||||
impl crate::commands::command_contract::CommandSpec for SnipeCommand {
|
||||
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
|
||||
crate::commands::command_contract::CommandMetadata {
|
||||
name: "snipe",
|
||||
category: "fun",
|
||||
params: "[index]",
|
||||
description: "Affiche le dernier message supprime dans le salon ou un index de messages supprimes.",
|
||||
examples: &["+snipe", "+se", "+help snipe"],
|
||||
default_aliases: &["snp"],
|
||||
allow_in_dm: false,
|
||||
default_permission: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
use chrono::Utc;
|
||||
use serenity::builder::{
|
||||
CreateActionRow, CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse,
|
||||
CreateInteractionResponseMessage, CreateMessage, CreateModal,
|
||||
};
|
||||
use serenity::model::Colour;
|
||||
use serenity::model::application::{
|
||||
ActionRowComponent, ButtonStyle, ComponentInteraction, InputTextStyle, ModalInteraction,
|
||||
};
|
||||
use serenity::model::prelude::*;
|
||||
use serenity::prelude::*;
|
||||
|
||||
use crate::commands::common::send_embed;
|
||||
use crate::db;
|
||||
|
||||
const SUGGESTION_MENU: &str = "suggestion:settings";
|
||||
|
||||
fn parse_owner_id(custom_id: &str) -> Option<(String, u64)> {
|
||||
let mut parts = custom_id.rsplitn(2, ':');
|
||||
let owner = parts.next()?.parse::<u64>().ok()?;
|
||||
let action = parts.next()?.to_string();
|
||||
Some((action, owner))
|
||||
}
|
||||
|
||||
fn modal_value(modal: &ModalInteraction, wanted_id: &str) -> Option<String> {
|
||||
for row in &modal.data.components {
|
||||
for component in &row.components {
|
||||
if let ActionRowComponent::InputText(input) = component {
|
||||
if input.custom_id == wanted_id {
|
||||
return input.value.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn suggestion_embed(author: &User, content: &str) -> CreateEmbed {
|
||||
CreateEmbed::new()
|
||||
.title("💡 Suggestion")
|
||||
.description(content)
|
||||
.colour(Colour::from_rgb(255, 200, 0))
|
||||
.author(serenity::builder::CreateEmbedAuthor::new(&author.name).icon_url(author.face()))
|
||||
.timestamp(Utc::now())
|
||||
}
|
||||
|
||||
fn suggestion_settings_embed(settings: &db::SuggestionSettings) -> CreateEmbed {
|
||||
let mut embed = CreateEmbed::new()
|
||||
.title("Gestion des suggestions")
|
||||
.description("Configure le système de suggestions du serveur.")
|
||||
.colour(Colour::from_rgb(255, 200, 0))
|
||||
.timestamp(Utc::now())
|
||||
.field(
|
||||
"Statut",
|
||||
if settings.enabled { "Actif" } else { "Inactif" },
|
||||
true,
|
||||
);
|
||||
|
||||
if let Some(channel_id) = settings.channel_id {
|
||||
embed = embed.field("Canal", format!("<#{}>", channel_id), true);
|
||||
}
|
||||
|
||||
if let Some(approve_channel_id) = settings.approve_channel_id {
|
||||
embed = embed.field("Validation", format!("<#{}>", approve_channel_id), true);
|
||||
}
|
||||
|
||||
embed
|
||||
}
|
||||
|
||||
fn suggestion_components(
|
||||
owner_id: UserId,
|
||||
settings: &db::SuggestionSettings,
|
||||
) -> Vec<CreateActionRow> {
|
||||
let toggle_label = if settings.enabled {
|
||||
"Désactiver"
|
||||
} else {
|
||||
"Activer"
|
||||
};
|
||||
|
||||
vec![CreateActionRow::Buttons(vec![
|
||||
CreateButton::new(format!("{}:submit:{}", SUGGESTION_MENU, owner_id.get()))
|
||||
.label("Soumettre")
|
||||
.style(ButtonStyle::Success),
|
||||
CreateButton::new(format!("{}:configure:{}", SUGGESTION_MENU, owner_id.get()))
|
||||
.label("Configurer")
|
||||
.style(ButtonStyle::Secondary),
|
||||
CreateButton::new(format!("{}:toggle:{}", SUGGESTION_MENU, owner_id.get()))
|
||||
.label(toggle_label)
|
||||
.style(ButtonStyle::Primary),
|
||||
CreateButton::new(format!("{}:refresh:{}", SUGGESTION_MENU, owner_id.get()))
|
||||
.label("Rafraîchir")
|
||||
.style(ButtonStyle::Secondary),
|
||||
])]
|
||||
}
|
||||
|
||||
async fn pool(ctx: &Context) -> Option<sqlx::PgPool> {
|
||||
let data = ctx.data.read().await;
|
||||
data.get::<db::DbPoolKey>().cloned()
|
||||
}
|
||||
|
||||
async fn show_menu(ctx: &Context, msg: &Message) {
|
||||
let Some(guild_id) = msg.guild_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(pool) = pool(ctx).await else {
|
||||
return;
|
||||
};
|
||||
|
||||
let bot_id = ctx.cache.current_user().id.get() as i64;
|
||||
let settings = db::get_or_create_suggestion_settings(&pool, bot_id, guild_id.get() as i64)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let Some(settings) = settings else {
|
||||
return;
|
||||
};
|
||||
|
||||
let _ = msg
|
||||
.channel_id
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
CreateMessage::new()
|
||||
.embed(suggestion_settings_embed(&settings))
|
||||
.components(suggestion_components(msg.author.id, &settings)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn submit_suggestion(
|
||||
ctx: &Context,
|
||||
guild_id: GuildId,
|
||||
author: &User,
|
||||
content: String,
|
||||
) -> Result<(), String> {
|
||||
let pool = pool(ctx)
|
||||
.await
|
||||
.ok_or_else(|| "Base de données indisponible".to_string())?;
|
||||
let bot_id = ctx.cache.current_user().id.get() as i64;
|
||||
let settings = db::get_or_create_suggestion_settings(&pool, bot_id, guild_id.get() as i64)
|
||||
.await
|
||||
.map_err(|e| format!("Erreur: {e}"))?;
|
||||
|
||||
if !settings.enabled {
|
||||
return Err("Le système de suggestions est désactivé.".to_string());
|
||||
}
|
||||
|
||||
let channel_id = settings
|
||||
.channel_id
|
||||
.ok_or_else(|| "Canal de suggestions non configuré".to_string())?;
|
||||
let channel = ChannelId::new(channel_id as u64)
|
||||
.to_channel(&ctx.http)
|
||||
.await
|
||||
.map_err(|e| format!("Erreur: {e}"))?;
|
||||
let guild_channel = channel
|
||||
.guild()
|
||||
.ok_or_else(|| "Canal de suggestions introuvable".to_string())?;
|
||||
|
||||
let message = guild_channel
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
serenity::builder::CreateMessage::new().embed(suggestion_embed(author, &content)),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Erreur: {e}"))?;
|
||||
|
||||
db::create_suggestion(
|
||||
&pool,
|
||||
bot_id,
|
||||
guild_id.get() as i64,
|
||||
channel_id,
|
||||
message.id.get() as i64,
|
||||
author.id.get() as i64,
|
||||
content.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Erreur: {e}"))?;
|
||||
|
||||
let _ = message.react(&ctx.http, '👍').await;
|
||||
let _ = message.react(&ctx.http, '👎').await;
|
||||
|
||||
if let Ok(channels) = db::get_autopublish_channels(&pool, bot_id, guild_id.get() as i64).await {
|
||||
for autopublish_channel in channels {
|
||||
if autopublish_channel.channel_id == channel_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
let _ = ChannelId::new(autopublish_channel.channel_id as u64)
|
||||
.send_message(
|
||||
&ctx.http,
|
||||
serenity::builder::CreateMessage::new()
|
||||
.embed(suggestion_embed(author, &content)),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_suggestion(ctx: &Context, msg: &Message, args: &[&str]) {
|
||||
if args
|
||||
.first()
|
||||
.map(|value| value.eq_ignore_ascii_case("settings"))
|
||||
.unwrap_or(false)
|
||||
{
|
||||
show_menu(ctx, msg).await;
|
||||
return;
|
||||
}
|
||||
|
||||
if args.is_empty() {
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Suggestion")
|
||||
.description("Utilisation: +suggestion <contenu> ou +suggestion settings")
|
||||
.color(0xED4245),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(guild_id) = msg.guild_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let content = args.join(" ");
|
||||
if content.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = submit_suggestion(ctx, guild_id, &msg.author, content).await {
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Suggestion")
|
||||
.description(error)
|
||||
.color(0xED4245),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
CreateEmbed::new()
|
||||
.title("Suggestion envoyée")
|
||||
.description("La suggestion a été publiée.")
|
||||
.colour(Colour::from_rgb(0, 200, 120))
|
||||
.timestamp(Utc::now()),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool {
|
||||
if !component.data.custom_id.starts_with(SUGGESTION_MENU) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some((action, owner_id)) = parse_owner_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 du menu peut l'utiliser.")
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(guild_id) = component.guild_id else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(pool) = pool(ctx).await else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let bot_id = ctx.cache.current_user().id.get() as i64;
|
||||
let settings = db::get_or_create_suggestion_settings(&pool, bot_id, guild_id.get() as i64)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let Some(settings) = settings else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if action.ends_with(":refresh") {
|
||||
let _ = component
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::UpdateMessage(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.embed(suggestion_settings_embed(&settings))
|
||||
.components(suggestion_components(component.user.id, &settings)),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
if action.ends_with(":toggle") {
|
||||
if let Ok(updated) = db::update_suggestion_settings(
|
||||
&pool,
|
||||
bot_id,
|
||||
guild_id.get() as i64,
|
||||
!settings.enabled,
|
||||
settings.channel_id,
|
||||
settings.approve_channel_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = component
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::UpdateMessage(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.embed(suggestion_settings_embed(&updated))
|
||||
.components(suggestion_components(component.user.id, &updated)),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if action.ends_with(":configure") {
|
||||
let modal = CreateModal::new(
|
||||
component.data.custom_id.clone(),
|
||||
"Configurer les suggestions",
|
||||
)
|
||||
.components(vec![
|
||||
CreateActionRow::InputText(
|
||||
CreateInputText::new(InputTextStyle::Short, "Canal des suggestions", "channel_id")
|
||||
.required(false),
|
||||
),
|
||||
CreateActionRow::InputText(
|
||||
CreateInputText::new(
|
||||
InputTextStyle::Short,
|
||||
"Canal d'approbation",
|
||||
"approve_channel_id",
|
||||
)
|
||||
.required(false),
|
||||
),
|
||||
]);
|
||||
|
||||
let _ = component
|
||||
.create_response(&ctx.http, CreateInteractionResponse::Modal(modal))
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
if action.ends_with(":submit") {
|
||||
let modal = CreateModal::new(component.data.custom_id.clone(), "Soumettre une suggestion")
|
||||
.components(vec![CreateActionRow::InputText(
|
||||
CreateInputText::new(InputTextStyle::Paragraph, "Contenu", "content")
|
||||
.required(true)
|
||||
.max_length(2000),
|
||||
)]);
|
||||
|
||||
let _ = component
|
||||
.create_response(&ctx.http, CreateInteractionResponse::Modal(modal))
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) -> bool {
|
||||
if !modal.data.custom_id.starts_with(SUGGESTION_MENU) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some((action, owner_id)) = parse_owner_id(&modal.data.custom_id) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if modal.user.id.get() != owner_id {
|
||||
let _ = modal
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("Seul l'auteur du menu peut soumettre ce formulaire.")
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(guild_id) = modal.guild_id else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let Some(pool) = pool(ctx).await else {
|
||||
return true;
|
||||
};
|
||||
|
||||
let bot_id = ctx.cache.current_user().id.get() as i64;
|
||||
let current = db::get_or_create_suggestion_settings(&pool, bot_id, guild_id.get() as i64)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
let Some(settings) = current else {
|
||||
return true;
|
||||
};
|
||||
|
||||
if action.ends_with(":configure") {
|
||||
let channel_id =
|
||||
modal_value(modal, "channel_id").and_then(|value| value.trim().parse::<i64>().ok());
|
||||
let approve_channel_id = modal_value(modal, "approve_channel_id")
|
||||
.and_then(|value| value.trim().parse::<i64>().ok());
|
||||
|
||||
if let Ok(updated) = db::update_suggestion_settings(
|
||||
&pool,
|
||||
bot_id,
|
||||
guild_id.get() as i64,
|
||||
settings.enabled,
|
||||
channel_id,
|
||||
approve_channel_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = modal
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.embed(suggestion_settings_embed(&updated))
|
||||
.components(suggestion_components(modal.user.id, &updated))
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if action.ends_with(":submit") {
|
||||
let content = modal_value(modal, "content").unwrap_or_default();
|
||||
if content.trim().is_empty() {
|
||||
let _ = modal
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("Contenu invalide.")
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
return true;
|
||||
}
|
||||
|
||||
match submit_suggestion(ctx, guild_id, &modal.user, content).await {
|
||||
Ok(_) => {
|
||||
let _ = modal
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content("Suggestion envoyée.")
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(error) => {
|
||||
let _ = modal
|
||||
.create_response(
|
||||
&ctx.http,
|
||||
CreateInteractionResponse::Message(
|
||||
CreateInteractionResponseMessage::new()
|
||||
.content(error)
|
||||
.ephemeral(true),
|
||||
),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub struct SuggestionCommand;
|
||||
pub static COMMAND_DESCRIPTOR: SuggestionCommand = SuggestionCommand;
|
||||
|
||||
impl crate::commands::command_contract::CommandSpec for SuggestionCommand {
|
||||
fn metadata(&self) -> crate::commands::command_contract::CommandMetadata {
|
||||
crate::commands::command_contract::CommandMetadata {
|
||||
name: "suggestion",
|
||||
category: "fun",
|
||||
params: "<contenu...> | settings",
|
||||
description: "Publie une suggestion utilisateur ou ouvre le panneau de configuration.",
|
||||
examples: &[
|
||||
"+suggestion Ameliorer le salon",
|
||||
"+suggestion settings",
|
||||
"+help suggestion",
|
||||
],
|
||||
default_aliases: &[],
|
||||
allow_in_dm: false,
|
||||
default_permission: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user