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
|
/target
|
||||||
/cli/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]
|
[workspace]
|
||||||
members = ["cli"]
|
members = ["cli", "discord"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.1"
|
version = "0.6.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"
|
||||||
|
|
|
||||||
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