feat: Initialized Discord bot

- Bump version to 0.6.0
- discord: Added /quest list
- discord: Added /quest create (admin)
This commit is contained in:
Alexey 2025-12-08 16:29:33 +03:00
commit 5fa2ac330f
8 changed files with 2334 additions and 12 deletions

2
.gitignore vendored
View file

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

2112
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

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

15
discord/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "squad-quest-discord"
version.workspace = true
edition.workspace = true
repository.workspace = true
license.workspace = true
[dependencies]
clap = { version = "4.5.53", features = ["derive"] }
dotenvy = "0.15.7"
poise = "0.6.1"
serde = "1.0.228"
squad-quest = { version = "0.5.1", path = ".." }
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
toml = "0.9.8"

10
discord/src/cli.rs Normal file
View file

@ -0,0 +1,10 @@
use std::path::PathBuf;
use clap::Parser;
#[derive(Parser)]
pub struct Cli {
/// Path to config.toml
#[arg(long, short)]
pub config: PathBuf,
}

View file

@ -0,0 +1,12 @@
//use poise::{CreateReply, serenity_prelude as serenity};
use crate::{Context, Error};
pub mod quest;
#[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), Error> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}

View file

@ -0,0 +1,148 @@
use std::str::FromStr;
use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}};
use toml::value::Date;
use crate::{Context, Error};
const ERROR_MSG: &str = "Server error :(";
#[poise::command(
prefix_command,
slash_command,
subcommands("list", "create"),
)]
pub async fn quest(
_ctx: Context<'_>,
) -> Result<(), Error> {
Ok(())
}
#[poise::command(
prefix_command,
slash_command,
)]
pub async fn list(
ctx: Context<'_>,
) -> Result<(), Error> {
let conf = &ctx.data().config;
let quests = conf.load_quests();
let mut reply_string = format!("Listing {} quests:", quests.len());
for quest in quests {
reply_string.push_str(format!("\n#{}: {}\n\tDescription: {}",
quest.id,
quest.name,
quest.description,
).as_str());
}
ctx.reply(reply_string).await?;
Ok(())
}
#[derive(Debug, poise::ChoiceParameter)]
pub enum DifficultyWrapper {
Easy,
Normal,
Hard,
Secret,
}
impl From<DifficultyWrapper> for QuestDifficulty {
fn from(value: DifficultyWrapper) -> Self {
match &value {
DifficultyWrapper::Easy => Self::Easy,
DifficultyWrapper::Normal => Self::Normal,
DifficultyWrapper::Hard => Self::Hard,
DifficultyWrapper::Secret => Self::Secret,
}
}
}
#[derive(serde::Deserialize)]
struct DateWrapper {
date: Date,
}
impl FromStr for DateWrapper {
type Err = toml::de::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let toml_str = format!("date = {s}");
let wrapper: Self = toml::from_str(&toml_str)?;
Ok(wrapper)
}
}
impl From<DateWrapper> for Date {
fn from(value: DateWrapper) -> Self {
value.date
}
}
#[poise::command(
prefix_command,
slash_command,
required_permissions = "ADMINISTRATOR",
guild_only,
)]
pub async fn create(
ctx: Context<'_>,
#[description = "Quest difficulty"]
difficulty: DifficultyWrapper,
#[description = "Reward for the quest"]
reward: u32,
#[description = "Quest name"]
name: String,
#[description = "Quest description"]
description: String,
#[description = "Quest answer, visible to admins"]
answer: String,
#[description = "Optional date of publication (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
available: Option<DateWrapper>,
#[description = "Optional deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"]
deadline: Option<DateWrapper>,
) -> Result<(), Error> {
let conf = &ctx.data().config;
let mut quests = conf.load_quests();
quests.sort_by(|a,b| a.id.cmp(&b.id));
let next_id = match quests.last() {
Some(quest) => quest.id + 1u16,
None => 0u16
};
let available_on = match available {
Some(avail) => Some(avail.into()),
None => None,
};
let deadline = match deadline {
Some(dl) => Some(dl.into()),
None => None,
};
let quest = Quest {
id: next_id,
difficulty: difficulty.into(),
reward,
name,
description,
answer,
public: false,
available_on,
deadline,
};
let path = conf.full_quests_path();
let reply_string = match quest.save(path) {
Ok(_) => format!("Created quest #{}", quest.id),
Err(error) => {
eprintln!("{error}");
format!("{ERROR_MSG}")
},
};
ctx.reply(reply_string).await?;
Ok(())
}

43
discord/src/main.rs Normal file
View file

@ -0,0 +1,43 @@
use clap::Parser;
use dotenvy::dotenv;
use poise::serenity_prelude as serenity;
use squad_quest::config::Config;
mod commands;
mod cli;
struct Data {
pub config: Config,
}
type Error = Box<dyn std::error::Error + Send + Sync>;
type Context<'a> = poise::Context<'a, Data, Error>;
#[tokio::main]
async fn main() {
dotenv().unwrap();
let cli = cli::Cli::parse();
let config = Config::load(cli.config.clone());
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()],
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(Data {config})
})
})
.build();
let client = serenity::ClientBuilder::new(token, intents)
.framework(framework)
.await;
client.unwrap().start().await.unwrap();
}