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
This commit is contained in:
Alexey 2025-12-09 21:15:50 +03:00
commit 520992187d
10 changed files with 147 additions and 12 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
/target /target
/cli/target /cli/target
/discord/target /discord/target
/discord/cfg
.env .env

6
Cargo.lock generated
View file

@ -1599,7 +1599,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest" name = "squad-quest"
version = "0.6.0" version = "0.7.0"
dependencies = [ dependencies = [
"serde", "serde",
"toml", "toml",
@ -1607,7 +1607,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest-cli" name = "squad-quest-cli"
version = "0.6.0" version = "0.7.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@ -1618,7 +1618,7 @@ dependencies = [
[[package]] [[package]]
name = "squad-quest-discord" name = "squad-quest-discord"
version = "0.6.0" version = "0.7.0"
dependencies = [ dependencies = [
"clap", "clap",
"dotenvy", "dotenvy",

View file

@ -2,7 +2,7 @@
members = ["cli", "discord"] members = ["cli", "discord"]
[workspace.package] [workspace.package]
version = "0.6.0" version = "0.7.0"
edition = "2024" edition = "2024"
repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest"
license = "MIT" license = "MIT"

View file

@ -9,5 +9,5 @@ license.workspace = true
chrono = "0.4.42" chrono = "0.4.42"
clap = { version = "4.5.53", features = ["derive"] } clap = { version = "4.5.53", features = ["derive"] }
serde = { version = "1.0.228", 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" toml = "0.9.8"

View file

@ -10,6 +10,6 @@ clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7" dotenvy = "0.15.7"
poise = "0.6.1" poise = "0.6.1"
serde = "1.0.228" 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"] } tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
toml = "0.9.8" toml = "0.9.8"

View file

@ -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(())
}

View file

@ -3,7 +3,9 @@
use crate::{Context, Error}; use crate::{Context, Error};
pub mod quest; pub mod quest;
pub mod init;
pub const ERROR_MSG: &str = "Server error :(";
#[poise::command(prefix_command)] #[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> { pub async fn register(ctx: Context<'_>) -> Result<(), Error> {

View file

@ -9,7 +9,7 @@ const ERROR_MSG: &str = "Server error :(";
#[poise::command( #[poise::command(
prefix_command, prefix_command,
slash_command, slash_command,
subcommands("list", "create"), subcommands("list", "create", "update"),
)] )]
pub async fn quest( pub async fn quest(
_ctx: Context<'_>, _ctx: Context<'_>,
@ -146,3 +146,82 @@ pub async fn create(
Ok(()) 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<DifficultyWrapper>,
#[description = "Reward for the quest"]
reward: Option<u32>,
#[description = "Quest name"]
name: Option<String>,
#[description = "Quest description"]
description: Option<String>,
#[description = "Quest answer, visible to admins"]
answer: Option<String>,
#[description = "Date of publication (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
available: Option<DateWrapper>,
#[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
deadline: Option<DateWrapper>,
#[description = "Clear availability and deadline if checked"]
#[rename = "override"]
should_override: Option<bool>,
) -> 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<Date>;
let dead_line: Option<Date>;
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(())
}

View file

@ -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 poise::serenity_prelude::{ChannelId, GuildId, MessageId};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
@ -6,18 +6,26 @@ use squad_quest::{SquadObject, config::Config, error::Error};
pub trait ConfigImpl { pub trait ConfigImpl {
fn discord_impl(&self) -> Result<DiscordConfig, Error>; fn discord_impl(&self) -> Result<DiscordConfig, Error>;
fn init_impl(&self) -> Result<(), Error>;
} }
impl ConfigImpl for Config { impl ConfigImpl for Config {
fn discord_impl(&self) -> Result<DiscordConfig, Error> { fn discord_impl(&self) -> Result<DiscordConfig, Error> {
let Some(path) = &self.impl_path else { let Some(path) = &self.full_impl_path() else {
return Err(Error::IsNotImplemented); return Err(Error::IsNotImplemented);
}; };
DiscordConfig::load(path.clone()) 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 struct DiscordConfig {
pub guild: GuildId, pub guild: GuildId,
pub quests_channel: ChannelId, pub quests_channel: ChannelId,

View file

@ -22,14 +22,21 @@ async fn main() {
let cli = cli::Cli::parse(); let cli = cli::Cli::parse();
let config = Config::load(cli.config.clone()); 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 token = std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN");
let intents = serenity::GatewayIntents::non_privileged(); let intents = serenity::GatewayIntents::non_privileged();
let framework = poise::Framework::builder() let framework = poise::Framework::builder()
.options(poise::FrameworkOptions { .options(poise::FrameworkOptions {
commands: vec![commands::quest::quest(), commands::register()], commands: vec![
commands::quest::quest(),
commands::register(),
commands::init::init()
],
..Default::default() ..Default::default()
}) })
.setup(|ctx, _ready, framework| { .setup(|ctx, _ready, framework| {