From 520992187d92da57f6d8e2b38c06afd21a624764 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 9 Dec 2025 21:15:50 +0300 Subject: [PATCH] feat: Implementation config - Bump version to 0.7.0 - Added Config::init_path - Added Error::IsNotImplemented - discord: added implementation config init/load - discord: added /init - discord: added /quest update --- .gitignore | 1 + Cargo.lock | 6 +-- Cargo.toml | 2 +- cli/Cargo.toml | 2 +- discord/Cargo.toml | 2 +- discord/src/commands/init.rs | 38 ++++++++++++++++ discord/src/commands/mod.rs | 2 + discord/src/commands/quest.rs | 81 ++++++++++++++++++++++++++++++++++- discord/src/config.rs | 14 ++++-- discord/src/main.rs | 11 ++++- 10 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 discord/src/commands/init.rs diff --git a/.gitignore b/.gitignore index 592b88f..f380615 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target /cli/target /discord/target +/discord/cfg .env diff --git a/Cargo.lock b/Cargo.lock index 0fd703d..7d0148e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1599,7 +1599,7 @@ dependencies = [ [[package]] name = "squad-quest" -version = "0.6.0" +version = "0.7.0" dependencies = [ "serde", "toml", @@ -1607,7 +1607,7 @@ dependencies = [ [[package]] name = "squad-quest-cli" -version = "0.6.0" +version = "0.7.0" dependencies = [ "chrono", "clap", @@ -1618,7 +1618,7 @@ dependencies = [ [[package]] name = "squad-quest-discord" -version = "0.6.0" +version = "0.7.0" dependencies = [ "clap", "dotenvy", diff --git a/Cargo.toml b/Cargo.toml index 1a76b11..95bbcce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["cli", "discord"] [workspace.package] -version = "0.6.0" +version = "0.7.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" license = "MIT" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index e9e3d70..b448666 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,5 +9,5 @@ license.workspace = true chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } -squad-quest = { version = "0.6.0", path = ".." } +squad-quest = { version = "0.7.0", path = ".." } toml = "0.9.8" diff --git a/discord/Cargo.toml b/discord/Cargo.toml index ee6abf5..ed6de15 100644 --- a/discord/Cargo.toml +++ b/discord/Cargo.toml @@ -10,6 +10,6 @@ clap = { version = "4.5.53", features = ["derive"] } dotenvy = "0.15.7" poise = "0.6.1" serde = "1.0.228" -squad-quest = { version = "0.6.0", path = ".." } +squad-quest = { version = "0.7.0", path = ".." } tokio = { version = "1.48.0", features = ["rt-multi-thread"] } toml = "0.9.8" diff --git a/discord/src/commands/init.rs b/discord/src/commands/init.rs new file mode 100644 index 0000000..1ac58ea --- /dev/null +++ b/discord/src/commands/init.rs @@ -0,0 +1,38 @@ +use std::path::Path; + +use poise::serenity_prelude::{ChannelId}; +use squad_quest::SquadObject; + +use crate::{commands::ERROR_MSG, Context, Error}; + + +#[poise::command( + prefix_command, + slash_command, + required_permissions = "ADMINISTRATOR", + guild_only, +)] +pub async fn init( + ctx: Context<'_>, + #[description = "Channel to post quests to"] + quests_channel: ChannelId, + #[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(_) => "Please restart bot to apply changes".to_string(), + Err(error) => { + eprintln!("{error}"); + ERROR_MSG.to_string() + }, + }; + ctx.reply(reply_string).await?; + + Ok(()) +} diff --git a/discord/src/commands/mod.rs b/discord/src/commands/mod.rs index 6b56b9c..37b33ea 100644 --- a/discord/src/commands/mod.rs +++ b/discord/src/commands/mod.rs @@ -3,7 +3,9 @@ use crate::{Context, Error}; pub mod quest; +pub mod init; +pub const ERROR_MSG: &str = "Server error :("; #[poise::command(prefix_command)] pub async fn register(ctx: Context<'_>) -> Result<(), Error> { diff --git a/discord/src/commands/quest.rs b/discord/src/commands/quest.rs index a5a44da..71fb536 100644 --- a/discord/src/commands/quest.rs +++ b/discord/src/commands/quest.rs @@ -9,7 +9,7 @@ const ERROR_MSG: &str = "Server error :("; #[poise::command( prefix_command, slash_command, - subcommands("list", "create"), + subcommands("list", "create", "update"), )] pub async fn quest( _ctx: Context<'_>, @@ -146,3 +146,82 @@ pub async fn create( Ok(()) } +#[poise::command( + prefix_command, + slash_command, + required_permissions = "ADMINISTRATOR", + guild_only, +)] +pub async fn update( + ctx: Context<'_>, + #[description = "Quest identifier"] + id: u16, + #[description = "Quest difficulty"] + difficulty: Option, + #[description = "Reward for the quest"] + reward: Option, + #[description = "Quest name"] + name: Option, + #[description = "Quest description"] + description: Option, + #[description = "Quest answer, visible to admins"] + answer: Option, + #[description = "Date of publication (in format of YYYY-MM-DD, e.g. 2025-12-24)"] + available: Option, + #[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"] + deadline: Option, + #[description = "Clear availability and deadline if checked"] + #[rename = "override"] + should_override: Option, +) -> Result<(), Error> { + let conf = &ctx.data().config; + let quests = conf.load_quests(); + let Some(quest) = quests.iter().find(|q| q.id == id) else { + let reply_string = format!("Quest #{id} not found"); + ctx.reply(reply_string).await?; + return Ok(()); + }; + + let difficulty = match difficulty { + Some(d) => d.into(), + None => quest.difficulty + }; + + let available_on: Option; + let dead_line: Option; + + match should_override.unwrap_or(false) { + true => { + available_on = available.map(|v| v.into()); + dead_line = deadline.map(|v| v.into()); + }, + false => { + available_on = available.map_or_else(|| quest.available_on.clone(), |v| Some(v.into())); + dead_line = deadline.map_or_else(|| quest.deadline.clone(), |v| Some(v.into())); + }, + } + + let new_quest = Quest { + id, + difficulty, + reward: reward.unwrap_or(quest.reward), + name: name.unwrap_or(quest.name.clone()), + description: description.unwrap_or(quest.description.clone()), + answer: answer.unwrap_or(quest.answer.clone()), + public: quest.public, + available_on, + deadline: dead_line, + }; + + let path = conf.full_quests_path(); + let reply_string = match new_quest.save(path) { + Err(error) => { + eprintln!("{error}"); + ERROR_MSG.to_string() + }, + Ok(_) => format!("Updated quest #{id}"), + }; + ctx.reply(reply_string).await?; + + Ok(()) +} diff --git a/discord/src/config.rs b/discord/src/config.rs index 8323e3d..de4013c 100644 --- a/discord/src/config.rs +++ b/discord/src/config.rs @@ -1,4 +1,4 @@ -use std::{io::Write, path::PathBuf}; +use std::{io::Write, path::{Path, PathBuf}}; use poise::serenity_prelude::{ChannelId, GuildId, MessageId}; use serde::{Serialize, Deserialize}; @@ -6,18 +6,26 @@ use squad_quest::{SquadObject, config::Config, error::Error}; pub trait ConfigImpl { fn discord_impl(&self) -> Result; + fn init_impl(&self) -> Result<(), Error>; } impl ConfigImpl for Config { fn discord_impl(&self) -> Result { - let Some(path) = &self.impl_path else { + let Some(path) = &self.full_impl_path() else { return Err(Error::IsNotImplemented); }; DiscordConfig::load(path.clone()) } + fn init_impl(&self) -> Result<(), Error> { + let Some(path) = self.full_impl_path() else { + return Err(Error::IsNotImplemented); + }; + let dc = DiscordConfig::default(); + dc.save(path.parent().unwrap_or(Path::new("")).to_owned()) + } } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Default, Clone)] pub struct DiscordConfig { pub guild: GuildId, pub quests_channel: ChannelId, diff --git a/discord/src/main.rs b/discord/src/main.rs index deae174..c73a2b5 100644 --- a/discord/src/main.rs +++ b/discord/src/main.rs @@ -22,14 +22,21 @@ async fn main() { let cli = cli::Cli::parse(); let config = Config::load(cli.config.clone()); - let discord = config.discord_impl().expect("config does not define impl_path"); + let discord = config.discord_impl().unwrap_or_else(|_| { + config.init_impl().unwrap(); + config.discord_impl().unwrap() + }); let token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN"); let intents = serenity::GatewayIntents::non_privileged(); let framework = poise::Framework::builder() .options(poise::FrameworkOptions { - commands: vec![commands::quest::quest(), commands::register()], + commands: vec![ + commands::quest::quest(), + commands::register(), + commands::init::init() + ], ..Default::default() }) .setup(|ctx, _ready, framework| {