From 1ae57ad358ebed476baecde916bc867a29b04ad7 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 10 Dec 2025 15:48:43 +0300 Subject: [PATCH] feat!: implemented answer buttons - Also you can /init without restarting bot BREAKING CHANGE: Changed type of Data::discord to Arc, removed field pending_answers from DiscordConfig --- discord/src/commands/answer.rs | 58 +++++++++++++++++++++++++++++----- discord/src/commands/init.rs | 28 +++++++++------- discord/src/config.rs | 1 - discord/src/main.rs | 6 ++-- 4 files changed, 70 insertions(+), 23 deletions(-) diff --git a/discord/src/commands/answer.rs b/discord/src/commands/answer.rs index eca42dd..beaa7bd 100644 --- a/discord/src/commands/answer.rs +++ b/discord/src/commands/answer.rs @@ -1,4 +1,4 @@ -use poise::serenity_prelude::{Attachment, CreateAttachment, CreateMessage, Mentionable}; +use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage, Mentionable}; use crate::{Context, Error}; @@ -14,7 +14,11 @@ pub async fn answer( #[description = "Text answer to the quest"] text: Option, #[description = "Attachment answer to the quest"] - files: Vec, + file1: Option, + #[description = "Attachment answer to the quest"] + file2: Option, + #[description = "Attachment answer to the quest"] + file3: Option, ) -> Result<(), Error> { let quests = ctx.data().config.load_quests(); let Some(quest) = quests.iter().find(|q| q.id == quest_id) else { @@ -22,6 +26,13 @@ pub async fn answer( ctx.reply(reply_string).await?; return Ok(()); }; + + let mut files: Vec = Vec::new(); + for file in [file1, file2, file3] { + if let Some(f) = file { + files.push(f); + } + } if text.is_none() && files.len() == 0 { let reply_string = "Please specify text or at least one attachment.".to_string(); @@ -38,7 +49,7 @@ pub async fn answer( "\nPassed answer has attachments.".to_string() }; - let content = format!("# From: {user}\n\ + let content = format!("## From: {user}\n\ ### Quest #{quest_id}: {quest_name}\n\ ### Expected answer:\n\ ||{quest_answer}||{text_ans}{attachment_notice}", @@ -53,16 +64,47 @@ pub async fn answer( let attachment = CreateAttachment::url(ctx, &file.url).await?; attachments.push(attachment); } + + let ctx_id = ctx.id(); + let approve_id = format!("{ctx_id}approve"); + let reject_id = format!("{ctx_id}reject"); - let ans_channel = ctx.data().discord.answers_channel; - let message = CreateMessage::new() - .content(content) - .files(attachments); + let components = CreateActionRow::Buttons(vec![ + CreateButton::new(&approve_id).label("Approve".to_string()), + CreateButton::new(&reject_id).label("Reject".to_string()), + ]); - ans_channel.send_message(ctx, message).await?; + let ans_channel = { + let discord = ctx.data().discord.clone(); + let guard = discord.lock().expect("should not be locked"); + guard.answers_channel + }; + let builder = CreateMessage::new() + .content(content.clone()) + .files(attachments) + .components(vec![components]); + + let mut message = ans_channel.send_message(ctx, builder).await?; let reply_string = "Your answer has been posted.".to_string(); ctx.reply(reply_string).await?; + while let Some(press) = ComponentInteractionCollector::new(ctx) + .filter(move |press| press.data.custom_id.starts_with(&ctx_id.to_string())) + .await + { + let admin = press.user.mention(); + let is_approved = press.data.custom_id == approve_id; + let content = if is_approved { + format!("{content}\nApproved by: {admin}") + } else { + format!("~~{content}~~\nRejected by: {admin}") + }; + + let builder = EditMessage::new().content(content).components(Vec::new()); + message.edit(ctx, builder).await?; + return Ok(()); + } + Ok(()) } diff --git a/discord/src/commands/init.rs b/discord/src/commands/init.rs index a0c4b9b..6b16bb5 100644 --- a/discord/src/commands/init.rs +++ b/discord/src/commands/init.rs @@ -19,18 +19,22 @@ pub async fn init( #[description = "Channel to post answers to check"] answers_channel: ChannelId, ) -> Result<(), Error> { - let mut dc = ctx.data().discord.clone(); - let guild = ctx.guild_id().unwrap(); - dc.quests_channel = quests_channel; - dc.answers_channel = answers_channel; - dc.guild = guild; - let path = &ctx.data().config.full_impl_path().unwrap(); - let reply_string = match dc.save(path.parent().unwrap_or(Path::new("")).to_owned()) { - Ok(_) => "Settings updated, please restart bot to apply changes.".to_string(), - Err(error) => { - eprintln!("{error}"); - ERROR_MSG.to_string() - }, + let reply_string = { + let dc = ctx.data().discord.clone(); + let mut guard = dc.lock().expect("shouldn't be locked"); + let guild = ctx.guild_id().unwrap(); + guard.quests_channel = quests_channel; + guard.answers_channel = answers_channel; + guard.guild = guild; + + let path = &ctx.data().config.full_impl_path().unwrap(); + match guard.save(path.parent().unwrap_or(Path::new("")).to_owned()) { + Ok(_) => "Settings updated.".to_string(), + Err(error) => { + eprintln!("{error}"); + ERROR_MSG.to_string() + }, + } }; ctx.reply(reply_string).await?; diff --git a/discord/src/config.rs b/discord/src/config.rs index 0708d53..6bcdf7b 100644 --- a/discord/src/config.rs +++ b/discord/src/config.rs @@ -10,7 +10,6 @@ pub struct DiscordConfig { pub quests_channel: ChannelId, pub answers_channel: ChannelId, pub quests_messages: Vec, - pub pending_answers: Vec, } pub trait ConfigImpl { diff --git a/discord/src/main.rs b/discord/src/main.rs index 950de6c..5792186 100644 --- a/discord/src/main.rs +++ b/discord/src/main.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use clap::Parser; use dotenvy::dotenv; use poise::serenity_prelude as serenity; @@ -11,7 +13,7 @@ mod config; struct Data { pub config: Config, - pub discord: DiscordConfig, + pub discord: Arc>, } type Error = Box; type Context<'a> = poise::Context<'a, Data, Error>; @@ -45,7 +47,7 @@ async fn main() { poise::builtins::register_globally(ctx, &framework.options().commands).await?; Ok(Data { config, - discord, + discord: Arc::new(Mutex::new(discord)), }) }) })