chore(commands): reorganize command files by metadata categories

This commit is contained in:
Puechberty Arthur
2026-04-10 15:25:21 +02:00
parent 23dcc69977
commit 1b5e51c428
154 changed files with 399 additions and 399 deletions
+107
View File
@@ -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,
}
}
}
+86
View File
@@ -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,
}
}
}
+81
View File
@@ -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,
}
}
}
+96
View File
@@ -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,
}
}
}
+112
View File
@@ -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,
}
}
}
+40
View File
@@ -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,
}
}
}
+82
View File
@@ -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,
}
}
}
+520
View File
@@ -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,
}
}
}