diff --git a/src/commands/admin/boostembed.rs b/src/commands/admin/boostembed.rs index 849b4b4..1d6283e 100644 --- a/src/commands/admin/boostembed.rs +++ b/src/commands/admin/boostembed.rs @@ -1,9 +1,638 @@ -use crate::commands::logs_service; +use serenity::builder::{ + CreateActionRow, CreateButton, CreateEmbed, CreateInputText, CreateInteractionResponse, + CreateInteractionResponseMessage, CreateMessage, CreateModal, +}; +use serenity::model::application::{ + ActionRowComponent, ButtonStyle, ComponentInteraction, InputTextStyle, ModalInteraction, +}; use serenity::model::prelude::*; use serenity::prelude::*; +use crate::commands::common::{parse_channel_id, send_embed, theme_color}; +use crate::commands::logs_service; +use crate::db::DbPoolKey; + +const BOOSTEMBED_MENU: &str = "boostembed:settings"; + +#[derive(Clone)] +struct BoostEmbedSettings { + enabled: bool, + title: Option, + description: Option, + color: Option, + boost_channel_id: Option, + boost_channel_enabled: bool, +} + +fn default_settings() -> BoostEmbedSettings { + BoostEmbedSettings { + enabled: true, + title: None, + description: None, + color: None, + boost_channel_id: None, + boost_channel_enabled: false, + } +} + +fn parse_owner_id(custom_id: &str) -> Option<(String, u64)> { + let mut parts = custom_id.rsplitn(2, ':'); + let owner = parts.next()?.parse::().ok()?; + let action = parts.next()?.to_string(); + Some((action, owner)) +} + +fn modal_value(modal: &ModalInteraction, wanted_id: &str) -> Option { + 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 +} + +async fn pool(ctx: &Context) -> Option { + let data = ctx.data.read().await; + data.get::().cloned() +} + +async fn ensure_boost_embed_row(pool: &sqlx::PgPool, bot_id: UserId, guild_id: GuildId) { + let _ = sqlx::query( + r#" + INSERT INTO bot_boost_embed (bot_id, guild_id, enabled, title, description, color) + VALUES ($1, $2, TRUE, NULL, NULL, NULL) + ON CONFLICT (bot_id, guild_id) + DO NOTHING; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .execute(pool) + .await; +} + +async fn set_boost_embed_enabled( + pool: &sqlx::PgPool, + bot_id: UserId, + guild_id: GuildId, + enabled: bool, +) { + let _ = sqlx::query( + r#" + INSERT INTO bot_boost_embed (bot_id, guild_id, enabled) + VALUES ($1, $2, $3) + ON CONFLICT (bot_id, guild_id) + DO UPDATE SET enabled = EXCLUDED.enabled, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(enabled) + .execute(pool) + .await; +} + +async fn set_boost_log_channel( + pool: &sqlx::PgPool, + bot_id: UserId, + guild_id: GuildId, + channel_id: Option, + enabled: bool, +) { + let _ = sqlx::query( + r#" + INSERT INTO bot_log_channels (bot_id, guild_id, log_type, channel_id, enabled) + VALUES ($1, $2, 'boost', $3, $4) + ON CONFLICT (bot_id, guild_id, log_type) + DO UPDATE SET channel_id = EXCLUDED.channel_id, enabled = EXCLUDED.enabled, updated_at = NOW(); + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(channel_id.map(|c| c.get() as i64)) + .bind(enabled) + .execute(pool) + .await; +} + +async fn read_settings(pool: &sqlx::PgPool, bot_id: UserId, guild_id: GuildId) -> BoostEmbedSettings { + let row = sqlx::query_as::<_, (bool, Option, Option, Option)>( + r#" + SELECT enabled, title, description, color + FROM bot_boost_embed + WHERE bot_id = $1 AND guild_id = $2 + LIMIT 1; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_optional(pool) + .await + .ok() + .flatten(); + + let channel_row = sqlx::query_as::<_, (Option, bool)>( + r#" + SELECT channel_id, enabled + FROM bot_log_channels + WHERE bot_id = $1 AND guild_id = $2 AND log_type = 'boost' + LIMIT 1; + "#, + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .fetch_optional(pool) + .await + .ok() + .flatten(); + + let mut settings = default_settings(); + if let Some((enabled, title, description, color)) = row { + settings.enabled = enabled; + settings.title = title; + settings.description = description; + settings.color = color; + } + + if let Some((channel_id, enabled)) = channel_row { + settings.boost_channel_id = channel_id; + settings.boost_channel_enabled = enabled; + } + + settings +} + +fn settings_embed(settings: &BoostEmbedSettings) -> CreateEmbed { + let channel_text = if settings.boost_channel_enabled { + settings + .boost_channel_id + .map(|id| format!("<#{}>", id)) + .unwrap_or_else(|| "activé mais salon non défini".to_string()) + } else { + "désactivé".to_string() + }; + + CreateEmbed::new() + .title("Configuration Boost Embed") + .description("Utilise les boutons/modals ci-dessous pour paramétrer l'embed de boost et son salon d'envoi.") + .field("Embed", if settings.enabled { "on" } else { "off" }, true) + .field("Salon d'envoi boost", channel_text, true) + .field( + "Titre", + settings + .title + .clone() + .unwrap_or_else(|| "(défaut)".to_string()), + false, + ) + .field( + "Description", + settings + .description + .clone() + .unwrap_or_else(|| "(défaut)".to_string()), + false, + ) + .field( + "Couleur", + settings + .color + .map(|v| format!("#{:06X}", v.max(0) as u32)) + .unwrap_or_else(|| "(défaut)".to_string()), + true, + ) + .color(0xF47FFF) +} + +fn settings_components(owner_id: UserId, settings: &BoostEmbedSettings) -> Vec { + let toggle_style = if settings.enabled { + ButtonStyle::Danger + } else { + ButtonStyle::Success + }; + + vec![ + CreateActionRow::Buttons(vec![ + CreateButton::new(format!("{}:toggle:{}", BOOSTEMBED_MENU, owner_id.get())) + .label(if settings.enabled { + "Désactiver embed" + } else { + "Activer embed" + }) + .style(toggle_style), + CreateButton::new(format!("{}:test:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Envoyer test") + .style(ButtonStyle::Primary), + CreateButton::new(format!("{}:refresh:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Rafraîchir") + .style(ButtonStyle::Secondary), + ]), + CreateActionRow::Buttons(vec![ + CreateButton::new(format!("{}:set_here:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Salon = ici") + .style(ButtonStyle::Success), + CreateButton::new(format!("{}:edit_channel:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Définir salon") + .style(ButtonStyle::Secondary), + CreateButton::new(format!("{}:disable_channel:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Couper envoi") + .style(ButtonStyle::Danger), + ]), + CreateActionRow::Buttons(vec![ + CreateButton::new(format!("{}:edit_title:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Modifier titre") + .style(ButtonStyle::Secondary), + CreateButton::new(format!("{}:edit_description:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Modifier description") + .style(ButtonStyle::Secondary), + CreateButton::new(format!("{}:edit_color:{}", BOOSTEMBED_MENU, owner_id.get())) + .label("Modifier couleur") + .style(ButtonStyle::Secondary), + ]), + ] +} + +async fn show_panel( + ctx: &Context, + msg: &Message, + pool: &sqlx::PgPool, + bot_id: UserId, + guild_id: GuildId, +) { + ensure_boost_embed_row(pool, bot_id, guild_id).await; + let settings = read_settings(pool, bot_id, guild_id).await; + let _ = msg + .channel_id + .send_message( + &ctx.http, + CreateMessage::new() + .embed(settings_embed(&settings)) + .components(settings_components(msg.author.id, &settings)), + ) + .await; +} + pub async fn handle_boostembed(ctx: &Context, msg: &Message, args: &[&str]) { - logs_service::handle_boostembed(ctx, msg, args).await; + let Some(guild_id) = msg.guild_id else { + return; + }; + + let Some(pool) = pool(ctx).await else { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BoostEmbed") + .description("DB indisponible.") + .color(0xED4245), + ) + .await; + return; + }; + + let bot_id = ctx.cache.current_user().id; + + if let Some(action) = args.first().map(|v| v.to_lowercase()) { + match action.as_str() { + "on" | "off" => { + set_boost_embed_enabled(&pool, bot_id, guild_id, action == "on").await; + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BoostEmbed") + .description(if action == "on" { "Activé." } else { "Désactivé." }) + .color(theme_color(ctx).await), + ) + .await; + return; + } + "test" => { + logs_service::send_boost_embed(ctx, guild_id, &msg.author).await; + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BoostEmbed") + .description("Test envoyé.") + .color(theme_color(ctx).await), + ) + .await; + return; + } + "settings" | "panel" => { + show_panel(ctx, msg, &pool, bot_id, guild_id).await; + return; + } + _ => { + send_embed( + ctx, + msg, + CreateEmbed::new() + .title("BoostEmbed") + .description("Usage: +boostembed [on|off|test|settings]") + .color(0xED4245), + ) + .await; + return; + } + } + } + + show_panel(ctx, msg, &pool, bot_id, guild_id).await; +} + +pub async fn handle_component_interaction(ctx: &Context, component: &ComponentInteraction) -> bool { + if !component.data.custom_id.starts_with(BOOSTEMBED_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 panneau 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 { + let _ = component + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content("DB indisponible.") + .ephemeral(true), + ), + ) + .await; + return true; + }; + + let bot_id = ctx.cache.current_user().id; + ensure_boost_embed_row(&pool, bot_id, guild_id).await; + + if action.ends_with(":edit_title") { + let modal = CreateModal::new( + format!("{}:modal:title:{}", BOOSTEMBED_MENU, component.user.id.get()), + "Modifier le titre du boost embed", + ) + .components(vec![CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Titre (vide = défaut)", "title") + .required(false), + )]); + + let _ = component + .create_response(&ctx.http, CreateInteractionResponse::Modal(modal)) + .await; + return true; + } + + if action.ends_with(":edit_description") { + let modal = CreateModal::new( + format!( + "{}:modal:description:{}", + BOOSTEMBED_MENU, + component.user.id.get() + ), + "Modifier la description du boost embed", + ) + .components(vec![CreateActionRow::InputText( + CreateInputText::new( + InputTextStyle::Paragraph, + "Description (vide = défaut)", + "description", + ) + .required(false), + )]); + + let _ = component + .create_response(&ctx.http, CreateInteractionResponse::Modal(modal)) + .await; + return true; + } + + if action.ends_with(":edit_color") { + let modal = CreateModal::new( + format!("{}:modal:color:{}", BOOSTEMBED_MENU, component.user.id.get()), + "Modifier la couleur du boost embed", + ) + .components(vec![CreateActionRow::InputText( + CreateInputText::new(InputTextStyle::Short, "Couleur hex (#FF66CC)", "color") + .required(false), + )]); + + let _ = component + .create_response(&ctx.http, CreateInteractionResponse::Modal(modal)) + .await; + return true; + } + + if action.ends_with(":edit_channel") { + let modal = CreateModal::new( + format!("{}:modal:channel:{}", BOOSTEMBED_MENU, component.user.id.get()), + "Définir le salon boost", + ) + .components(vec![CreateActionRow::InputText( + CreateInputText::new( + InputTextStyle::Short, + "Salon (#mention ou ID, vide = désactiver)", + "channel", + ) + .required(false), + )]); + + let _ = component + .create_response(&ctx.http, CreateInteractionResponse::Modal(modal)) + .await; + return true; + } + + if action.ends_with(":toggle") { + let settings = read_settings(&pool, bot_id, guild_id).await; + set_boost_embed_enabled(&pool, bot_id, guild_id, !settings.enabled).await; + } else if action.ends_with(":set_here") { + set_boost_log_channel(&pool, bot_id, guild_id, Some(component.channel_id), true).await; + } else if action.ends_with(":disable_channel") { + set_boost_log_channel(&pool, bot_id, guild_id, None, false).await; + } else if action.ends_with(":test") { + logs_service::send_boost_embed(ctx, guild_id, &component.user).await; + } + + let settings = read_settings(&pool, bot_id, guild_id).await; + let _ = component + .create_response( + &ctx.http, + CreateInteractionResponse::UpdateMessage( + CreateInteractionResponseMessage::new() + .embed(settings_embed(&settings)) + .components(settings_components(component.user.id, &settings)), + ), + ) + .await; + + true +} + +pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) -> bool { + if !modal.data.custom_id.starts_with(&format!("{}:modal:", BOOSTEMBED_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 panneau peut l'utiliser.") + .ephemeral(true), + ), + ) + .await; + return true; + } + + let Some(guild_id) = modal.guild_id else { + return true; + }; + + let Some(pool) = pool(ctx).await else { + let _ = modal + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content("DB indisponible.") + .ephemeral(true), + ), + ) + .await; + return true; + }; + + let bot_id = ctx.cache.current_user().id; + ensure_boost_embed_row(&pool, bot_id, guild_id).await; + + if action.ends_with(":modal:title") { + let title = modal_value(modal, "title").unwrap_or_default(); + let title_value = if title.trim().is_empty() { + None + } else { + Some(title) + }; + + let _ = sqlx::query( + "UPDATE bot_boost_embed SET title = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(title_value) + .execute(&pool) + .await; + } else if action.ends_with(":modal:description") { + let description = modal_value(modal, "description").unwrap_or_default(); + let desc_value = if description.trim().is_empty() { + None + } else { + Some(description) + }; + + let _ = sqlx::query( + "UPDATE bot_boost_embed SET description = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(desc_value) + .execute(&pool) + .await; + } else if action.ends_with(":modal:color") { + let raw = modal_value(modal, "color").unwrap_or_default(); + let color_value = if raw.trim().is_empty() { + None + } else { + let normalized = raw.trim().trim_start_matches('#').trim_start_matches("0x"); + match u32::from_str_radix(normalized, 16) { + Ok(value) => Some(value as i32), + Err(_) => { + let _ = modal + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content("Couleur invalide. Exemple: `#FF66CC`") + .ephemeral(true), + ), + ) + .await; + return true; + } + } + }; + + let _ = sqlx::query( + "UPDATE bot_boost_embed SET color = $3, updated_at = NOW() WHERE bot_id = $1 AND guild_id = $2", + ) + .bind(bot_id.get() as i64) + .bind(guild_id.get() as i64) + .bind(color_value) + .execute(&pool) + .await; + } else if action.ends_with(":modal:channel") { + let raw = modal_value(modal, "channel").unwrap_or_default(); + if raw.trim().is_empty() { + set_boost_log_channel(&pool, bot_id, guild_id, None, false).await; + } else if let Some(channel) = parse_channel_id(&raw) { + set_boost_log_channel(&pool, bot_id, guild_id, Some(channel), true).await; + } else { + let _ = modal + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content("Salon invalide. Donne une mention `#salon` ou un ID.") + .ephemeral(true), + ), + ) + .await; + return true; + } + } + + let _ = modal + .create_response( + &ctx.http, + CreateInteractionResponse::Message( + CreateInteractionResponseMessage::new() + .content("Configuration boost embed mise à jour. Clique sur `Rafraîchir` dans le panneau.") + .ephemeral(true), + ), + ) + .await; + + true } pub struct BoostembedCommand; @@ -15,10 +644,10 @@ impl crate::commands::command_contract::CommandSpec for BoostembedCommand { key: "boostembed", command: "boostembed", category: "admin", - params: "", - summary: "Active, coupe ou teste l embed boost", - description: "Controle l embed de boost et permet un test rapide.", - examples: &["+boostembed on", "+boostembed test"], + params: "[on|off|test|settings]", + summary: "Configure l embed boost avec panneau interactif", + description: "Ouvre un panneau avec composants pour paramétrer l'embed boost et le salon où il est envoyé.", + examples: &["+boostembed", "+boostembed settings", "+boostembed test"], alias_source_key: "boostembed", default_aliases: &["bembed"], default_permission: 8, diff --git a/src/commands/admin/rename.rs b/src/commands/admin/rename.rs index 8631d67..6d60e51 100644 --- a/src/commands/admin/rename.rs +++ b/src/commands/admin/rename.rs @@ -90,7 +90,10 @@ pub async fn handle_rename(ctx: &Context, msg: &Message, args: &[&str]) { if msg .channel_id - .edit(&ctx.http, serenity::builder::EditChannel::new().name(new_name.clone())) + .edit( + &ctx.http, + serenity::builder::EditChannel::new().name(new_name.clone()), + ) .await .is_err() { diff --git a/src/commands/admin/suggestion.rs b/src/commands/admin/suggestion.rs index 03ccb0a..b20f6d3 100644 --- a/src/commands/admin/suggestion.rs +++ b/src/commands/admin/suggestion.rs @@ -3,10 +3,10 @@ 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::Colour; use serenity::model::prelude::*; use serenity::prelude::*; @@ -40,9 +40,7 @@ fn suggestion_embed(author: &User, content: &str) -> CreateEmbed { .title("💡 Suggestion") .description(content) .colour(Colour::from_rgb(255, 200, 0)) - .author( - serenity::builder::CreateEmbedAuthor::new(&author.name).icon_url(author.face()), - ) + .author(serenity::builder::CreateEmbedAuthor::new(&author.name).icon_url(author.face())) .timestamp(Utc::now()) } @@ -52,7 +50,11 @@ fn suggestion_settings_embed(settings: &db::SuggestionSettings) -> CreateEmbed { .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); + .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); @@ -65,8 +67,15 @@ fn suggestion_settings_embed(settings: &db::SuggestionSettings) -> CreateEmbed { embed } -fn suggestion_components(owner_id: UserId, settings: &db::SuggestionSettings) -> Vec { - let toggle_label = if settings.enabled { "Désactiver" } else { "Activer" }; +fn suggestion_components( + owner_id: UserId, + settings: &db::SuggestionSettings, +) -> Vec { + let toggle_label = if settings.enabled { + "Désactiver" + } else { + "Activer" + }; vec![CreateActionRow::Buttons(vec![ CreateButton::new(format!("{}:submit:{}", SUGGESTION_MENU, owner_id.get())) @@ -124,7 +133,9 @@ async fn submit_suggestion( author: &User, content: String, ) -> Result<(), String> { - let pool = pool(ctx).await.ok_or_else(|| "Base de données indisponible".to_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 @@ -134,12 +145,16 @@ async fn submit_suggestion( 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_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 guild_channel = channel + .guild() + .ok_or_else(|| "Canal de suggestions introuvable".to_string())?; let message = guild_channel .send_message( @@ -184,7 +199,11 @@ async fn submit_suggestion( } 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) { + if args + .first() + .map(|value| value.eq_ignore_ascii_case("settings")) + .unwrap_or(false) + { show_menu(ctx, msg).await; return; } @@ -316,17 +335,24 @@ pub async fn handle_component_interaction(ctx: &Context, component: &ComponentIn } 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 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)) @@ -392,8 +418,8 @@ pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) - }; if action.ends_with(":configure") { - let channel_id = modal_value(modal, "channel_id") - .and_then(|value| value.trim().parse::().ok()); + let channel_id = + modal_value(modal, "channel_id").and_then(|value| value.trim().parse::().ok()); let approve_channel_id = modal_value(modal, "approve_channel_id") .and_then(|value| value.trim().parse::().ok()); diff --git a/src/commands/admin/tempvoc.rs b/src/commands/admin/tempvoc.rs index 251bb0a..95f168e 100644 --- a/src/commands/admin/tempvoc.rs +++ b/src/commands/admin/tempvoc.rs @@ -3,10 +3,10 @@ use serenity::builder::{ CreateActionRow, CreateButton, CreateChannel, CreateEmbed, CreateInputText, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, CreateModal, }; +use serenity::model::Colour; use serenity::model::application::{ ActionRowComponent, ButtonStyle, ComponentInteraction, InputTextStyle, ModalInteraction, }; -use serenity::model::Colour; use serenity::model::prelude::*; use serenity::prelude::*; @@ -40,7 +40,11 @@ fn tempvoc_embed(settings: &db::TempvocSettings) -> CreateEmbed { .description("Gère les vocaux temporaires du serveur.") .colour(Colour::from_rgb(100, 180, 255)) .timestamp(Utc::now()) - .field("Statut", if settings.enabled { "Actif" } else { "Inactif" }, true); + .field( + "Statut", + if settings.enabled { "Actif" } else { "Inactif" }, + true, + ); if let Some(trigger) = settings.trigger_channel_id { embed = embed.field("Canal déclencheur", format!("<#{}>", trigger), true); @@ -54,7 +58,11 @@ fn tempvoc_embed(settings: &db::TempvocSettings) -> CreateEmbed { } fn tempvoc_components(owner_id: UserId, settings: &db::TempvocSettings) -> Vec { - let toggle_label = if settings.enabled { "Désactiver" } else { "Activer" }; + let toggle_label = if settings.enabled { + "Désactiver" + } else { + "Activer" + }; vec![CreateActionRow::Buttons(vec![ CreateButton::new(format!("{}:toggle:{}", TEMPVOC_MENU, owner_id.get())) @@ -254,10 +262,10 @@ pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) - }; let bot_id = ctx.cache.current_user().id.get() as i64; - let trigger_channel_id = modal_value(modal, "trigger_channel_id") - .and_then(|value| value.trim().parse::().ok()); - let category_id = modal_value(modal, "category_id") - .and_then(|value| value.trim().parse::().ok()); + let trigger_channel_id = + modal_value(modal, "trigger_channel_id").and_then(|value| value.trim().parse::().ok()); + let category_id = + modal_value(modal, "category_id").and_then(|value| value.trim().parse::().ok()); let updated = db::update_tempvoc_settings( &pool, @@ -317,14 +325,20 @@ async fn cached_room_members(ctx: &Context, guild_id: GuildId, channel_id: Chann .unwrap_or(0) } -async fn create_temp_channel(ctx: &Context, guild_id: GuildId, user: &User, settings: &db::TempvocSettings) { +async fn create_temp_channel( + ctx: &Context, + guild_id: GuildId, + user: &User, + settings: &db::TempvocSettings, +) { let Some(trigger_channel_id) = settings.trigger_channel_id else { return; }; let Ok(trigger_channel) = ChannelId::new(trigger_channel_id as u64) .to_channel(&ctx.http) - .await else { + .await + else { return; }; diff --git a/src/commands/admin/ticket.rs b/src/commands/admin/ticket.rs index a70ad10..b80a620 100644 --- a/src/commands/admin/ticket.rs +++ b/src/commands/admin/ticket.rs @@ -1,13 +1,13 @@ use chrono::Utc; +use serenity::all::{PermissionOverwrite, PermissionOverwriteType, Permissions}; use serenity::builder::{ CreateActionRow, CreateButton, CreateChannel, CreateEmbed, CreateInputText, CreateInteractionResponse, CreateInteractionResponseMessage, CreateMessage, CreateModal, }; +use serenity::model::Colour; use serenity::model::application::{ ActionRowComponent, ButtonStyle, ComponentInteraction, InputTextStyle, ModalInteraction, }; -use serenity::all::{PermissionOverwrite, PermissionOverwriteType, Permissions}; -use serenity::model::Colour; use serenity::model::prelude::*; use serenity::prelude::*; @@ -58,7 +58,11 @@ fn ticket_embed(settings: &db::TicketSettings) -> CreateEmbed { .description("Utilise les boutons ci-dessous pour gérer le système de tickets.") .colour(Colour::from_rgb(90, 160, 255)) .timestamp(Utc::now()) - .field("Statut", if settings.enabled { "Actif" } else { "Inactif" }, true); + .field( + "Statut", + if settings.enabled { "Actif" } else { "Inactif" }, + true, + ); if let Some(category_id) = settings.category_id { embed = embed.field("Catégorie", format!("<#{}>", category_id), true); @@ -72,7 +76,11 @@ fn ticket_embed(settings: &db::TicketSettings) -> CreateEmbed { } fn ticket_components(owner_id: UserId, settings: &db::TicketSettings) -> Vec { - let toggle_label = if settings.enabled { "Désactiver" } else { "Activer" }; + let toggle_label = if settings.enabled { + "Désactiver" + } else { + "Activer" + }; vec![CreateActionRow::Buttons(vec![ CreateButton::new(format!("{}:create:{}", TICKET_MENU, owner_id.get())) @@ -130,7 +138,9 @@ async fn create_ticket_channel( title: String, settings: &db::TicketSettings, ) -> Result { - let pool = pool(ctx).await.ok_or_else(|| "Base de données indisponible".to_string())?; + let pool = pool(ctx) + .await + .ok_or_else(|| "Base de données indisponible".to_string())?; let name = sanitize_channel_name(&title); if name.is_empty() { @@ -344,10 +354,10 @@ pub async fn handle_modal_interaction(ctx: &Context, modal: &ModalInteraction) - }; if action.ends_with(":configure") { - let category_id = modal_value(modal, "category_id") - .and_then(|value| value.trim().parse::().ok()); - let log_channel_id = modal_value(modal, "log_channel_id") - .and_then(|value| value.trim().parse::().ok()); + let category_id = + modal_value(modal, "category_id").and_then(|value| value.trim().parse::().ok()); + let log_channel_id = + modal_value(modal, "log_channel_id").and_then(|value| value.trim().parse::().ok()); if let Ok(updated) = db::update_ticket_settings( &pool, diff --git a/src/commands/admin/ticket_member.rs b/src/commands/admin/ticket_member.rs index 2baa25e..19f7689 100644 --- a/src/commands/admin/ticket_member.rs +++ b/src/commands/admin/ticket_member.rs @@ -16,9 +16,10 @@ const TICKET_ALLOW: Permissions = Permissions::VIEW_CHANNEL .union(Permissions::ADD_REACTIONS); fn ticket_member_id(args: &[&str], msg: &Message) -> Option { - msg.mentions.first().map(|user| user.id).or_else(|| { - args.first().and_then(|value| parse_user_id(value)) - }) + msg.mentions + .first() + .map(|user| user.id) + .or_else(|| args.first().and_then(|value| parse_user_id(value))) } async fn ticket_member_update( @@ -81,9 +82,9 @@ async fn ticket_member_update( }; let mut overwrites = guild_channel.permission_overwrites.clone(); - overwrites.retain(|overwrite| { - !matches!(overwrite.kind, PermissionOverwriteType::Member(id) if id == user_id) - }); + overwrites.retain( + |overwrite| !matches!(overwrite.kind, PermissionOverwriteType::Member(id) if id == user_id), + ); if allow { overwrites.push(PermissionOverwrite { @@ -117,7 +118,11 @@ async fn ticket_member_update( let _ = db::remove_ticket_member(&pool, ticket.id, user_id.get() as i64).await; } - let title = if allow { "Membre ajouté" } else { "Membre retiré" }; + let title = if allow { + "Membre ajouté" + } else { + "Membre retiré" + }; let description = if allow { format!("<@{}> a été ajouté au ticket.", user_id.get()) } else { diff --git a/src/commands/admin/tickets.rs b/src/commands/admin/tickets.rs index d91303b..e978180 100644 --- a/src/commands/admin/tickets.rs +++ b/src/commands/admin/tickets.rs @@ -29,15 +29,9 @@ pub async fn handle_tickets(ctx: &Context, msg: &Message, args: &[&str]) { let offset = (page - 1) * limit; let bot_id = ctx.cache.current_user().id.get() as i64; - let tickets = db::get_guild_tickets( - &pool, - bot_id, - guild_id.get() as i64, - limit, - offset, - ) - .await - .unwrap_or_default(); + let tickets = db::get_guild_tickets(&pool, bot_id, guild_id.get() as i64, limit, offset) + .await + .unwrap_or_default(); if tickets.is_empty() { send_embed( diff --git a/src/commands/general/help.rs b/src/commands/general/help.rs index 3cece1c..4f468fa 100644 --- a/src/commands/general/help.rs +++ b/src/commands/general/help.rs @@ -161,8 +161,9 @@ fn help_page_for_command( "owner" | "unowner" | "clear_owners" | "bl" | "unbl" | "blinfo" | "clear_bl" | "allbots" | "alladmins" | "botadmins" | "mainprefix" | "prefix" | "mp" | "invite" | "leave" | "discussion" => "administration", - "perms" | "del" | "clear_perms" | "allperms" | "alias" | "help" - | "helpsetting" => "permissions", + "perms" | "del" | "clear_perms" | "allperms" | "alias" | "help" | "helpsetting" => { + "permissions" + } _ => match meta.category { "general" => "infos", "profile" => "bot", @@ -487,7 +488,9 @@ fn help_page_content( } else { lines.push(format!( "`+{}`{} - {} · alias: `{}`", - label, permission, summary, + label, + permission, + summary, aliases.join("`, `") )); } diff --git a/src/commands/general/showpics.rs b/src/commands/general/showpics.rs index 74b3f89..a1b2b05 100644 --- a/src/commands/general/showpics.rs +++ b/src/commands/general/showpics.rs @@ -15,8 +15,14 @@ pub async fn handle_show_pics(ctx: &Context, msg: &Message, args: &[&str]) { return; }; - let members = guild.members(ctx, Some(200), None).await.unwrap_or_default(); - let members: Vec<_> = members.into_iter().filter(|member| !member.user.bot).collect(); + let members = guild + .members(ctx, Some(200), None) + .await + .unwrap_or_default(); + let members: Vec<_> = members + .into_iter() + .filter(|member| !member.user.bot) + .collect(); if members.is_empty() { send_embed( diff --git a/src/commands/logs/viewlogs.rs b/src/commands/logs/viewlogs.rs index e73fe27..dcc1f2b 100644 --- a/src/commands/logs/viewlogs.rs +++ b/src/commands/logs/viewlogs.rs @@ -6,7 +6,7 @@ use serenity::prelude::*; use crate::commands::common::{send_embed, theme_color}; use crate::db::DbPoolKey; -const LOGS_PER_PAGE: i64 = 5; +const LOGS_PER_PAGE: i64 = 10; pub async fn pool(ctx: &Context) -> Option { let data = ctx.data.read().await; diff --git a/src/commands/logs_service.rs b/src/commands/logs_service.rs index 2ed234e..0176094 100644 --- a/src/commands/logs_service.rs +++ b/src/commands/logs_service.rs @@ -569,102 +569,6 @@ async fn run_join_leave_action(ctx: &Context, guild_id: GuildId, kind: &str, use let _ = channel_id.say(&ctx.http, content).await; } -pub async fn handle_boostembed(ctx: &Context, msg: &Message, args: &[&str]) { - 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; - - if let Some(action) = args.first().map(|v| v.to_lowercase()) { - match action.as_str() { - "on" | "off" => { - let enabled = action == "on"; - let _ = sqlx::query( - r#" - INSERT INTO bot_boost_embed (bot_id, guild_id, enabled) - VALUES ($1, $2, $3) - ON CONFLICT (bot_id, guild_id) - DO UPDATE SET enabled = EXCLUDED.enabled, updated_at = NOW(); - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .bind(enabled) - .execute(&pool) - .await; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("BoostEmbed") - .description(if enabled { "Activé" } else { "Désactivé" }) - .color(theme_color(ctx).await), - ) - .await; - return; - } - "test" => { - send_boost_embed(ctx, guild_id, &msg.author).await; - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("BoostEmbed") - .description("Test envoyé.") - .color(theme_color(ctx).await), - ) - .await; - return; - } - _ => {} - } - } - - let row = sqlx::query_as::<_, (bool, Option, Option, Option)>( - r#" - SELECT enabled, title, description, color - FROM bot_boost_embed - WHERE bot_id = $1 AND guild_id = $2 - LIMIT 1; - "#, - ) - .bind(bot_id.get() as i64) - .bind(guild_id.get() as i64) - .fetch_optional(&pool) - .await - .ok() - .flatten(); - - let desc = if let Some((enabled, title, description, color)) = row { - format!( - "État: {}\nTitle: {}\nDescription: {}\nColor: {}", - if enabled { "on" } else { "off" }, - title.unwrap_or_else(|| "(défaut)".to_string()), - description.unwrap_or_else(|| "(défaut)".to_string()), - color - .map(|v| format!("#{:06X}", v.max(0) as u32)) - .unwrap_or_else(|| "(thème)".to_string()) - ) - } else { - "Aucun réglage boost embed.".to_string() - }; - - send_embed( - ctx, - msg, - CreateEmbed::new() - .title("BoostEmbed") - .description(desc) - .color(theme_color(ctx).await), - ) - .await; -} - pub async fn handle_set_boostembed(ctx: &Context, msg: &Message, args: &[&str]) { let Some(guild_id) = msg.guild_id else { return; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index d51c13c..cf72f12 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -17,6 +17,8 @@ pub mod allperms; pub mod autobackup; #[path = "admin/autoconfiglog.rs"] pub mod autoconfiglog; +#[path = "admin/autopublish.rs"] +pub mod autopublish; #[path = "admin/autoreact.rs"] pub mod autoreact; #[path = "admin/backup.rs"] @@ -55,6 +57,8 @@ pub mod changeall; pub mod channel; #[path = "general/choose.rs"] pub mod choose; +#[path = "admin/claim.rs"] +pub mod claim; #[path = "admin/cleanup.rs"] pub mod cleanup; #[path = "admin/clear_all_sanctions.rs"] @@ -69,6 +73,8 @@ pub mod clear_owners; pub mod clear_perms; #[path = "admin/clear_sanctions.rs"] pub mod clear_sanctions; +#[path = "admin/close.rs"] +pub mod close; #[path = "admin/cmute.rs"] pub mod cmute; pub mod command_contract; @@ -168,6 +174,8 @@ pub mod prefix; pub mod raidlog; #[path = "profile/remove_activity.rs"] pub mod remove_activity; +#[path = "admin/rename.rs"] +pub mod rename; #[path = "admin/renew.rs"] pub mod renew; #[path = "admin/reroll.rs"] @@ -194,10 +202,14 @@ pub mod set_boostembed; pub mod set_modlogs; #[path = "general/shadowbot.rs"] pub mod shadowbot; +#[path = "general/showpics.rs"] +pub mod showpics; #[path = "general/snipe.rs"] pub mod snipe; #[path = "profile/stream.rs"] pub mod stream; +#[path = "admin/suggestion.rs"] +pub mod suggestion; #[path = "admin/sync.rs"] pub mod sync; #[path = "admin/tempban.rs"] @@ -208,8 +220,18 @@ pub mod tempcmute; pub mod tempmute; #[path = "admin/temprole.rs"] pub mod temprole; +#[path = "admin/tempvoc.rs"] +pub mod tempvoc; +#[path = "admin/tempvoc_cmd.rs"] +pub mod tempvoc_cmd; #[path = "profile/theme.rs"] pub mod theme; +#[path = "admin/ticket.rs"] +pub mod ticket; +#[path = "admin/ticket_member.rs"] +pub mod ticket_member; +#[path = "admin/tickets.rs"] +pub mod tickets; #[path = "admin/unban.rs"] pub mod unban; #[path = "admin/unbanall.rs"] @@ -244,34 +266,12 @@ pub mod viewlogs; pub mod vocinfo; #[path = "admin/voicekick.rs"] pub mod voicekick; -#[path = "admin/ticket.rs"] -pub mod ticket; -#[path = "admin/tickets.rs"] -pub mod tickets; -#[path = "general/showpics.rs"] -pub mod showpics; -#[path = "admin/suggestion.rs"] -pub mod suggestion; -#[path = "admin/autopublish.rs"] -pub mod autopublish; -#[path = "admin/tempvoc.rs"] -pub mod tempvoc; -#[path = "admin/tempvoc_cmd.rs"] -pub mod tempvoc_cmd; #[path = "admin/voicelog.rs"] pub mod voicelog; #[path = "admin/voicemove.rs"] pub mod voicemove; #[path = "admin/warn.rs"] pub mod warn; -#[path = "admin/claim.rs"] -pub mod claim; -#[path = "admin/close.rs"] -pub mod close; -#[path = "admin/rename.rs"] -pub mod rename; -#[path = "admin/ticket_member.rs"] -pub mod ticket_member; #[path = "profile/watch.rs"] pub mod watch; diff --git a/src/commands/permissions/helpsetting.rs b/src/commands/permissions/helpsetting.rs index 70b0e37..7499541 100644 --- a/src/commands/permissions/helpsetting.rs +++ b/src/commands/permissions/helpsetting.rs @@ -77,7 +77,14 @@ fn build_embed(settings: &HelpSettingsData) -> CreateEmbed { .field("Mode d'affichage", format!("`{}`", settings.layout), true) .field( "Aliases", - format!("`{}`", if settings.aliases_enabled { "on" } else { "off" }), + format!( + "`{}`", + if settings.aliases_enabled { + "on" + } else { + "off" + } + ), true, ) .field( @@ -166,7 +173,11 @@ fn build_components(owner_id: UserId, settings: &HelpSettingsData) -> Vec Result<(), s Ok(()) } - diff --git a/src/events/interaction_create_event.rs b/src/events/interaction_create_event.rs index 9151517..2adba48 100644 --- a/src/events/interaction_create_event.rs +++ b/src/events/interaction_create_event.rs @@ -2,7 +2,8 @@ use serenity::model::prelude::*; use serenity::prelude::*; use crate::commands::{ - advanced_tools, help, helpsetting, mp, perms_service, suggestion, tempvoc, ticket, + advanced_tools, boostembed, help, helpsetting, mp, perms_service, suggestion, tempvoc, + ticket, }; pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) { @@ -21,6 +22,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) return; } + if boostembed::handle_component_interaction(ctx, component).await { + return; + } + if tempvoc::handle_component_interaction(ctx, component).await { return; } @@ -54,6 +59,10 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) return; } + if boostembed::handle_modal_interaction(ctx, modal).await { + return; + } + if tempvoc::handle_modal_interaction(ctx, modal).await { return; } @@ -61,5 +70,3 @@ pub async fn handle_interaction_create(ctx: &Context, interaction: &Interaction) let _ = advanced_tools::handle_modal_interaction(ctx, modal).await; } } - - diff --git a/src/events/message_event.rs b/src/events/message_event.rs index 99985c3..8332712 100644 --- a/src/events/message_event.rs +++ b/src/events/message_event.rs @@ -6,19 +6,19 @@ use std::sync::{Mutex, OnceLock}; use crate::commands::moderation_tools; use crate::commands::remove_activity; use crate::commands::{ - addrole, alias, autobackup, autoconfiglog, autoreact, backup, ban, banlist, banner, bl, blinfo, - boostembed, boosters, boostlog, bringall, button, calc, change, changeall, channel, choose, - cleanup, clear_all_sanctions, clear_bl, clear_messages, clear_owners, clear_perms, - clear_sanctions, claim, cmute, compet, create, del, del_sanction, delrole, derank, discussion, - dnd, embed, emoji, end, giveaway, help, helpsetting, hide, hideall, idle, invisible, - invite, join, kick, leave, leave_settings, listen, loading, lock, lockall, mainprefix, - massiverole, member, messagelog, modlog, mp, mute, mutelist, newsticker, nolog, online, owner, - perms, pic, ping, playto, prefix, raidlog, renew, rename, reroll, role, rolelog, rolemembers, - sanctions, say, server, serverinfo, set, set_boostembed, set_modlogs, shadowbot, showpics, - snipe, stream, sync, suggestion, tempban, tempcmute, tempmute, temprole, tempvoc, theme, tickets, - unban, unbanall, unbl, uncmute, unhide, unhideall, unlock, unlockall, unmassiverole, unmute, - unmuteall, unowner, untemprole, user, viewlogs, vocinfo, voicekick, voicelog, voicemove, warn, - watch, autopublish, ticket, ticket_member, close, tempvoc_cmd, + addrole, alias, autobackup, autoconfiglog, autopublish, autoreact, backup, ban, banlist, + banner, bl, blinfo, boostembed, boosters, boostlog, bringall, button, calc, change, changeall, + channel, choose, claim, cleanup, clear_all_sanctions, clear_bl, clear_messages, clear_owners, + clear_perms, clear_sanctions, close, cmute, compet, create, del, del_sanction, delrole, derank, + discussion, dnd, embed, emoji, end, giveaway, help, helpsetting, hide, hideall, idle, + invisible, invite, join, kick, leave, leave_settings, listen, loading, lock, lockall, + mainprefix, massiverole, member, messagelog, modlog, mp, mute, mutelist, newsticker, nolog, + online, owner, perms, pic, ping, playto, prefix, raidlog, rename, renew, reroll, role, rolelog, + rolemembers, sanctions, say, server, serverinfo, set, set_boostembed, set_modlogs, shadowbot, + showpics, snipe, stream, suggestion, sync, tempban, tempcmute, tempmute, temprole, tempvoc, + tempvoc_cmd, theme, ticket, ticket_member, tickets, unban, unbanall, unbl, uncmute, unhide, + unhideall, unlock, unlockall, unmassiverole, unmute, unmuteall, unowner, untemprole, user, + viewlogs, vocinfo, voicekick, voicelog, voicemove, warn, watch, }; use crate::commands::{alladmins, allbots, allperms, botadmins}; use crate::db::{DbPoolKey, upsert_message_observed}; @@ -145,12 +145,22 @@ pub async fn handle_message(ctx: &Context, msg: &Message) { "del" => ticket_member::handle_ticket_remove(ctx, msg, &args).await, "close" => close::handle_close(ctx, msg, &args).await, "tickets" => tickets::handle_tickets(ctx, msg, &args).await, - "show" if args.first().map(|s| s.eq_ignore_ascii_case("pics")).unwrap_or(false) => { + "show" + if args + .first() + .map(|s| s.eq_ignore_ascii_case("pics")) + .unwrap_or(false) => + { showpics::handle_show_pics(ctx, msg, &args[1..]).await } "suggestion" => suggestion::handle_suggestion(ctx, msg, &args).await, "autopublish" => autopublish::handle_autopublish(ctx, msg, &args).await, - "tempvoc" if args.first().map(|s| s.eq_ignore_ascii_case("cmd")).unwrap_or(false) => { + "tempvoc" + if args + .first() + .map(|s| s.eq_ignore_ascii_case("cmd")) + .unwrap_or(false) => + { tempvoc_cmd::handle_tempvoc_cmd(ctx, msg, &args[1..]).await } "tempvoc" => tempvoc::handle_tempvoc(ctx, msg, &args).await,