feat: Initialized Discord bot
- Bump version to 0.6.0 - discord: Added /quest list - discord: Added /quest create (admin)
This commit is contained in:
parent
2960b6dfc4
commit
5fa2ac330f
8 changed files with 2334 additions and 12 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
|||
/target
|
||||
/cli/target
|
||||
/discord/target
|
||||
.env
|
||||
|
|
|
|||
2112
Cargo.lock
generated
2112
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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
15
discord/Cargo.toml
Normal 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
10
discord/src/cli.rs
Normal 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,
|
||||
}
|
||||
12
discord/src/commands/mod.rs
Normal file
12
discord/src/commands/mod.rs
Normal 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(())
|
||||
}
|
||||
148
discord/src/commands/quest.rs
Normal file
148
discord/src/commands/quest.rs
Normal 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
43
discord/src/main.rs
Normal 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();
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue