From 46af205aefbf706cfd1b6aef40c617f28a2ecd54 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Dec 2025 14:17:02 +0300 Subject: [PATCH 1/9] style: Fixed several minor things in text - Changed error in /move to CannotReach instead of RoomNotFound --- discord/src/commands/account.rs | 2 +- discord/src/commands/init.rs | 5 ++++- discord/src/commands/map.rs | 2 +- discord/src/commands/quest.rs | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index 5f2896e..d47b906 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -63,7 +63,7 @@ pub async fn reset( prefix_command, slash_command, guild_only, - name_localized("ru", "счёт"), + name_localized("ru", "счет"), description_localized("ru", "Отобразить таблицу лидеров"), )] pub async fn scoreboard( diff --git a/discord/src/commands/init.rs b/discord/src/commands/init.rs index e85d79a..fb6e6c3 100644 --- a/discord/src/commands/init.rs +++ b/discord/src/commands/init.rs @@ -76,10 +76,13 @@ fn seconds(time: Time) -> u64 { required_permissions = "ADMINISTRATOR", guild_only, name_localized("ru", "таймер"), - description_localized("ru", "Включить таймер публикации по заданной временной метке UTC (МСК-3)"), + description_localized("ru", "Включить таймер публикации по заданной временной метке UTC (МСК -3)"), )] pub async fn timer( ctx: Context<'_>, + #[description = "UTC time (in format HH:MM:SS, e.g. 9:00:00)"] + #[name_localized("ru", "время")] + #[description_localized("ru", "Время по UTC (МСК -3) в формате ЧЧ:ММ:СС, напр. 9:00:00")] time: TimeWrapper, ) -> Result<(), Error> { if ctx.data().has_timer() { diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index 4313132..96bb30d 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -71,7 +71,7 @@ pub async fn r#move( let mut account = fetch_or_init_account(conf, acc_id); if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) { - return Err(Error::RoomNotFound(id)); + return Err(Error::CannotReach(id)); } account.location = id; diff --git a/discord/src/commands/quest.rs b/discord/src/commands/quest.rs index bf90fdd..ee6089d 100644 --- a/discord/src/commands/quest.rs +++ b/discord/src/commands/quest.rs @@ -36,7 +36,7 @@ fn make_quest_message_content(ctx: Context<'_>, quest: &Quest) -> String { guild_only, subcommands("list", "create", "update", "publish", "delete"), required_permissions = "ADMINISTRATOR", - name_localized("ru", "квесты"), + name_localized("ru", "квест"), )] pub async fn quest( _ctx: Context<'_>, From 9d1261b74d2ddaa442d33b4a90b4b2c745f195f5 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Dec 2025 16:22:02 +0300 Subject: [PATCH 2/9] build: Preparing stuff to create debian package - Added deb binary target to generate incomplete control file - Added CLI init option to insert impl_path in config --- Cargo.toml | 2 ++ cli/src/cli/mod.rs | 2 ++ cli/src/main.rs | 1 + src/bin/deb.rs | 24 ++++++++++++++++++++++++ 4 files changed, 29 insertions(+) create mode 100644 src/bin/deb.rs diff --git a/Cargo.toml b/Cargo.toml index 5f06182..e55b3ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = ["cli", "discord"] version = "0.10.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" +homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest" license = "MIT" [package] @@ -13,6 +14,7 @@ edition.workspace = true version.workspace = true repository.workspace = true license.workspace = true +homepage.workspace = true [dependencies] serde = { version = "1.0.228", features = ["derive"] } diff --git a/cli/src/cli/mod.rs b/cli/src/cli/mod.rs index db78d60..5ff05fa 100644 --- a/cli/src/cli/mod.rs +++ b/cli/src/cli/mod.rs @@ -40,4 +40,6 @@ pub enum Objects { pub struct InitArgs { #[arg(long,short)] pub path: Option, + #[arg(long,short)] + pub implpath: Option, } diff --git a/cli/src/main.rs b/cli/src/main.rs index d120a9d..c1c75d5 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -71,6 +71,7 @@ fn main() { let config = Config { path: path.clone(), + impl_path: args.implpath.clone(), ..Default::default() }; diff --git a/src/bin/deb.rs b/src/bin/deb.rs new file mode 100644 index 0000000..b4938d7 --- /dev/null +++ b/src/bin/deb.rs @@ -0,0 +1,24 @@ +//! This binary generates DEBIAN/control text for use in debian package +use std::process::Command; + +fn main() { + let version = env!("CARGO_PKG_VERSION"); + let homepage = env!("CARGO_PKG_HOMEPAGE"); + let dpkg_arch = { + let output = match Command::new("dpkg") + .arg("--print-architecture") + .output() { + Ok(out) => out, + Err(error) => panic!("error running dpkg: {error}"), + }; + String::from_utf8(output.stdout).expect("dpkg returned ill UTF-8") + }; + println!("Package: squad-quest\n\ + Version: {version}-1\n\ + Architecture: {dpkg_arch}\ + Section: misc\n\ + Priority: optional\n\ + Homepage: {homepage}\n\ + Description: Simple RPG-like system for hosting events\n\ + Maintainer: Alexey Mirenkov <2ndbeam@disroot.org>"); +} From 66cbd2301369ae698e0b2c7128e52c5d8562e784 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Dec 2025 16:58:50 +0300 Subject: [PATCH 3/9] style: Changed name in license --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 4eee171..4e5488f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2025 (c) 2ndbeam +Copyright 2025 (c) Alexey Mirenkov <2ndbeam@disroot.org> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 0ab777d898edb986f6a9d68c9cbeca989f18bb4d Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Fri, 19 Dec 2025 17:15:13 +0300 Subject: [PATCH 4/9] build: Added unfinished build-deb.sh --- build-deb.sh | 23 +++++++++++++++++++++++ src/bin/deb.rs | 3 ++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100755 build-deb.sh diff --git a/build-deb.sh b/build-deb.sh new file mode 100755 index 0000000..2c5b7a9 --- /dev/null +++ b/build-deb.sh @@ -0,0 +1,23 @@ +#!/bin/sh +cargo build --workspace --release + +install -dvm755 target/release/dpkg/etc/squad_quest target/release/dpkg/usr/bin target/release/dpkg/DEBIAN target/release/dpkg/usr/share/doc/squad-quest + +strip target/release/squad-quest-cli +strip target/release/squad-quest-discord +install -vm755 target/release/squad-quest-cli target/release/squad-quest-discord target/release/dpkg/usr/bin + +install -vm 644 LICENSE target/release/dpkg/usr/share/doc/squad-quest/copyright + +target/release/squad-quest-cli -qc nil init -i discord.toml -p target/release/dpkg/etc/squad_quest +cargo build --bin deb --release +target/release/deb > target/release/dpkg/DEBIAN/control + +echo -n "" > target/release/dpkg/DEBIAN/conffiles +for file in $(ls target/release/dpkg/etc/squad_quest); do + if [ -f target/release/dpkg/etc/squad_quest/$file ]; then + echo "/etc/squad_quest/$file" >> target/release/dpkg/DEBIAN/conffiles + fi +done + +dpkg-deb --root-owner-group --build target/release/dpkg target/release/squad-quest.deb diff --git a/src/bin/deb.rs b/src/bin/deb.rs index b4938d7..281d003 100644 --- a/src/bin/deb.rs +++ b/src/bin/deb.rs @@ -19,6 +19,7 @@ fn main() { Section: misc\n\ Priority: optional\n\ Homepage: {homepage}\n\ - Description: Simple RPG-like system for hosting events\n\ + Description: Simple RPG-like system for hosting events\n \ + Includes discord bot and CLI\n\ Maintainer: Alexey Mirenkov <2ndbeam@disroot.org>"); } From 81a9ec0c50468cd6cdd4192323b6303bf7dd32d6 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Sun, 21 Dec 2025 11:08:08 +0300 Subject: [PATCH 5/9] feat: Added message context to strings.quest.publish --- discord/src/commands/quest.rs | 14 +++++++++----- discord/src/strings.rs | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/discord/src/commands/quest.rs b/discord/src/commands/quest.rs index ee6089d..1d73544 100644 --- a/discord/src/commands/quest.rs +++ b/discord/src/commands/quest.rs @@ -310,7 +310,7 @@ pub async fn update( Ok(()) } -pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<(), Error> { +pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result { quest.public = true; let quests_path = ctx.data().config.full_quests_path(); @@ -327,8 +327,10 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result<(), Er guard.quests_channel }; - channel.send_message(ctx, builder).await?; - Ok(()) + match channel.send_message(ctx, builder).await { + Ok(m) => Ok(m), + Err(error) => Err(Error::SerenityError(error)), + } } /// Mark quest as public and send its message in quests channel @@ -357,10 +359,12 @@ pub async fn publish( return Err(Error::QuestIsPublic(id)); } - publish_inner(ctx, quest).await?; + let message = publish_inner(ctx, quest).await?; let strings = &ctx.data().strings; - let formatter = strings.formatter().quest(&quest); + let formatter = strings.formatter() + .quest(&quest) + .message(&message); let reply_string = formatter.fmt(&strings.quest.publish); ctx.reply(reply_string).await?; diff --git a/discord/src/strings.rs b/discord/src/strings.rs index a3d485f..afa2ac8 100644 --- a/discord/src/strings.rs +++ b/discord/src/strings.rs @@ -328,7 +328,7 @@ impl Default for QuestStrings { list_item: "{n}{q.id}: {q.name}{n} Description: {q.description}".to_string(), create: "Created quest {q.id}".to_string(), update: "Updated quest {q.id}".to_string(), - publish: "Published quest {q.id}: {text}".to_string(), + publish: "Published quest {q.id}: {m.link}".to_string(), delete: "Deleted quest {q.id}".to_string(), message_format: "### `{q.id}` {q.name} (+{q.reward}){n}\ Difficulty: *{q.difficulty}*{n}\ From c22787792d6dc59307f1e7c0117a06808d57fff4 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 24 Dec 2025 14:30:40 +0300 Subject: [PATCH 6/9] feat: Added API for web map in discord bot - Bump version to 0.11.0 - Added data table to quests, accounts and rooms - Discord bot now adds "avatar" and "name" data to accounts on init - Added CLI "map data" command --- Cargo.lock | 1152 +++++++++++++++++++++++++------ Cargo.toml | 2 +- cli/Cargo.toml | 2 +- cli/src/cli/map.rs | 8 + cli/src/main.rs | 15 +- discord/Cargo.toml | 4 +- discord/Rocket.toml | 8 + discord/src/account.rs | 16 +- discord/src/api.rs | 119 ++++ discord/src/commands/account.rs | 3 +- discord/src/commands/answer.rs | 2 +- discord/src/commands/map.rs | 4 +- discord/src/main.rs | 15 +- src/account/mod.rs | 6 +- src/config/mod.rs | 2 +- src/map/mod.rs | 7 +- src/quest/mod.rs | 10 +- tests/main.rs | 10 +- 18 files changed, 1161 insertions(+), 224 deletions(-) create mode 100644 discord/Rocket.toml create mode 100644 discord/src/api.rs diff --git a/Cargo.lock b/Cargo.lock index b846c1b..388a656 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,28 @@ dependencies = [ "serde", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -96,18 +118,33 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "atomic" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -115,10 +152,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "bitflags" -version = "1.3.2" +name = "binascii" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] name = "bitflags" @@ -137,9 +174,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytecount" @@ -147,6 +184,12 @@ version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + [[package]] name = "byteorder" version = "1.5.0" @@ -161,9 +204,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -192,9 +235,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.48" +version = "1.2.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" dependencies = [ "find-msvc-tools", "shlex", @@ -206,6 +249,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.42" @@ -267,13 +316,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] -name = "core-foundation" -version = "0.9.4" +name = "cookie" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "core-foundation-sys", - "libc", + "percent-encoding", + "time", + "version_check", ] [[package]] @@ -401,6 +451,39 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "devise" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d" +dependencies = [ + "devise_codegen", + "devise_core", +] + +[[package]] +name = "devise_codegen" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867" +dependencies = [ + "devise_core", + "quote", +] + +[[package]] +name = "devise_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" +dependencies = [ + "bitflags", + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.111", +] + [[package]] name = "digest" version = "0.10.7" @@ -428,6 +511,12 @@ version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -468,6 +557,20 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic 0.6.1", + "pear", + "serde", + "toml 0.8.23", + "uncased", + "version_check", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" @@ -577,12 +680,16 @@ dependencies = [ ] [[package]] -name = "fxhash" -version = "0.2.1" +name = "generator" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" dependencies = [ - "byteorder", + "cc", + "libc", + "log", + "rustversion", + "windows", ] [[package]] @@ -602,8 +709,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -613,9 +722,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -661,6 +772,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "0.2.12" @@ -693,6 +810,29 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + [[package]] name = "httparse" version = "1.10.1" @@ -717,7 +857,7 @@ dependencies = [ "futures-util", "h2", "http 0.2.12", - "http-body", + "http-body 0.4.6", "httparse", "httpdate", "itoa", @@ -730,17 +870,65 @@ dependencies = [ ] [[package]] -name = "hyper-rustls" -version = "0.24.2" +name = "hyper" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ - "futures-util", - "http 0.2.12", - "hyper", - "rustls 0.21.12", + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", "tokio", - "tokio-rustls 0.24.1", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.35", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.4", +] + +[[package]] +name = "hyper-util" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.1", + "tokio", + "tower-service", + "tracing", ] [[package]] @@ -815,9 +1003,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -829,9 +1017,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -883,14 +1071,43 @@ checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", + "serde", + "serde_core", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -899,9 +1116,9 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] name = "js-sys" @@ -914,10 +1131,16 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.177" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "linux-raw-sys" @@ -942,9 +1165,39 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] [[package]] name = "memchr" @@ -995,15 +1248,43 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "multer" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.4.0", + "httparse", + "memchr", + "mime", + "spin", + "tokio", + "tokio-util", + "version_check", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1019,6 +1300,16 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -1054,6 +1345,29 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.111", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -1134,17 +1448,85 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "version_check", + "yansi", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.10.0", + "bitflags", "memchr", "unicase", ] +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.35", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.35", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.1", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.42" @@ -1167,8 +1549,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1178,7 +1570,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1190,13 +1592,42 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "redox_syscall" version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] @@ -1230,46 +1661,44 @@ checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.21.7", + "base64", "bytes", - "encoding_rs", "futures-core", "futures-util", - "h2", - "http 0.2.12", - "http-body", - "hyper", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", "hyper-rustls", - "ipnet", + "hyper-util", "js-sys", "log", - "mime", "mime_guess", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile", + "quinn", + "rustls 0.23.35", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.26.4", "tokio-util", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.25.4", - "winreg", + "webpki-roots 1.0.4", ] [[package]] @@ -1287,30 +1716,106 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "1.1.2" +name = "rocket" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f" dependencies = [ - "bitflags 2.10.0", + "async-stream", + "async-trait", + "atomic 0.5.3", + "binascii", + "bytes", + "either", + "figment", + "futures", + "indexmap", + "log", + "memchr", + "multer", + "num_cpus", + "parking_lot", + "pin-project-lite", + "rand 0.8.5", + "ref-cast", + "rocket_codegen", + "rocket_http", + "serde", + "serde_json", + "state", + "tempfile", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "ubyte", + "version_check", + "yansi", +] + +[[package]] +name = "rocket_codegen" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" +dependencies = [ + "devise", + "glob", + "indexmap", + "proc-macro2", + "quote", + "rocket_http", + "syn 2.0.111", + "unicode-xid", + "version_check", +] + +[[package]] +name = "rocket_http" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" +dependencies = [ + "cookie", + "either", + "futures", + "http 0.2.12", + "hyper 0.14.32", + "indexmap", + "log", + "memchr", + "pear", + "percent-encoding", + "pin-project-lite", + "ref-cast", + "serde", + "smallvec", + "stable-pattern", + "state", + "time", + "tokio", + "uncased", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.22.4" @@ -1326,31 +1831,27 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls" +version = "0.23.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.8", + "subtle", "zeroize", ] [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "rustls-pki-types" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" dependencies = [ - "ring", - "untrusted", + "web-time", + "zeroize", ] [[package]] @@ -1364,6 +1865,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -1372,9 +1884,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" [[package]] name = "same-file" @@ -1385,22 +1897,18 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "secrecy" version = "0.8.0" @@ -1462,9 +1970,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.146" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8" dependencies = [ "itoa", "memchr", @@ -1475,9 +1983,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ "serde_core", ] @@ -1496,24 +2013,24 @@ dependencies = [ [[package]] name = "serenity" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d72ec4323681bf9a3cabe40fd080abc2435859b502a1b5aa9bf693f125bfa76" +checksum = "9bde37f42765dfdc34e2a039e0c84afbf79a3101c1941763b0beb816c2f17541" dependencies = [ "arrayvec", "async-trait", - "base64 0.22.1", - "bitflags 2.10.0", + "base64", + "bitflags", "bytes", "chrono", "dashmap", "flate2", "futures", - "fxhash", "mime_guess", "parking_lot", "percent-encoding", "reqwest", + "rustc-hash", "secrecy", "serde", "serde_cow", @@ -1538,6 +2055,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -1545,10 +2071,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "simd-adler32" -version = "0.3.7" +name = "signal-hook-registry" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "skeptic" @@ -1597,37 +2132,54 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "squad-quest" -version = "0.10.0" +version = "0.11.0" dependencies = [ "serde", - "toml", + "toml 0.9.10+spec-1.1.0", ] [[package]] name = "squad-quest-cli" -version = "0.10.0" +version = "0.11.0" dependencies = [ "chrono", "clap", "serde", "squad-quest", - "toml", + "toml 0.9.10+spec-1.1.0", ] [[package]] name = "squad-quest-discord" -version = "0.10.0" +version = "0.11.0" dependencies = [ "chrono", "clap", "dotenvy", "poise", + "rocket", "serde", + "serde_json", "squad-quest", "tokio", - "toml", + "toml 0.9.10+spec-1.1.0", +] + +[[package]] +name = "stable-pattern" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" +dependencies = [ + "memchr", ] [[package]] @@ -1636,6 +2188,15 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "state" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" +dependencies = [ + "loom", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1672,9 +2233,12 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1687,27 +2251,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" @@ -1733,7 +2276,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", ] [[package]] @@ -1747,6 +2299,26 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.44" @@ -1788,6 +2360,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.48.0" @@ -1798,6 +2385,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2 0.6.1", "tokio-macros", "windows-sys 0.61.2", @@ -1814,16 +2402,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -1835,6 +2413,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.35", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-tungstenite" version = "0.21.0" @@ -1866,14 +2465,26 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "0.9.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ "indexmap", "serde_core", - "serde_spanned", - "toml_datetime", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow", @@ -1881,27 +2492,95 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ "serde_core", ] [[package]] -name = "toml_parser" -version = "1.0.4" +name = "toml_edit" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" dependencies = [ "winnow", ] [[package]] -name = "toml_writer" -version = "1.0.4" +name = "toml_write" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" @@ -1911,9 +2590,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -1934,11 +2613,41 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1965,11 +2674,11 @@ dependencies = [ "http 1.4.0", "httparse", "log", - "rand", + "rand 0.8.5", "rustls 0.22.4", "rustls-pki-types", "sha1", - "thiserror", + "thiserror 1.0.69", "url", "utf-8", ] @@ -2015,6 +2724,25 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" +dependencies = [ + "serde", +] + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "serde", + "version_check", +] + [[package]] name = "unicase" version = "2.8.1" @@ -2027,6 +2755,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -2063,6 +2797,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -2185,10 +2925,14 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.25.4" +name = "web-time" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] [[package]] name = "webpki-roots" @@ -2217,6 +2961,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-core" version = "0.62.2" @@ -2276,15 +3029,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -2503,15 +3247,8 @@ name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "memchr", ] [[package]] @@ -2526,6 +3263,15 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +dependencies = [ + "is-terminal", +] + [[package]] name = "yoke" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index e55b3ab..bffc69b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["cli", "discord"] [workspace.package] -version = "0.10.0" +version = "0.11.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 1db73ec..ac8581c 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.10.0", path = ".." } +squad-quest = { version = "0.11.0", path = ".." } toml = "0.9.8" diff --git a/cli/src/cli/map.rs b/cli/src/cli/map.rs index c98a158..539b4c8 100644 --- a/cli/src/cli/map.rs +++ b/cli/src/cli/map.rs @@ -14,6 +14,8 @@ pub enum MapCommands { Delete(MapDeleteArgs), /// Update room data Update(MapUpdateArgs), + /// Get room implementation data + Data(MapDataArgs), } #[derive(Args)] @@ -55,3 +57,9 @@ pub struct MapUpdateArgs { #[arg(short,long)] pub value: Option, } + +#[derive(Args)] +pub struct MapDataArgs { + /// Room ID + pub id: u16, +} diff --git a/cli/src/main.rs b/cli/src/main.rs index c1c75d5..eb6dceb 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -147,7 +147,8 @@ fn main() { answer: args.answer.clone(), public: args.public, available_on: args.available.clone(), - deadline: args.deadline.clone() + deadline: args.deadline.clone(), + ..Default::default() }; do_and_log(quest.save(path), !cli.quiet, format!("Created quest #{}.", quest.id)); @@ -169,7 +170,8 @@ fn main() { answer: args.answer.clone().unwrap_or(quest.answer.clone()), public: args.public.unwrap_or(quest.public), available_on: args.available.clone().or(quest.available_on.clone()), - deadline: args.deadline.clone().or(quest.deadline.clone()) + deadline: args.deadline.clone().or(quest.deadline.clone()), + ..Default::default() }; do_and_log(quest.save(path), !cli.quiet, format!("Updated quest #{}.", quest.id)); @@ -445,6 +447,15 @@ fn main() { let connected = if connect { "Connected" } else { "Disconnected" }; do_and_log(map_save(map, map_path), !cli.quiet, format!("{connected} rooms #{} <-> #{}.", args.first, args.second)); }, + MapCommands::Data(args) => { + if let Some(room) = map.room.iter().find(|r| r.id == args.id) { + if let Some(data) = &room.data { + for (key, value) in data { + println!("{key} = {value}"); + } + } + } + }, } } } diff --git a/discord/Cargo.toml b/discord/Cargo.toml index 6853c25..510a366 100644 --- a/discord/Cargo.toml +++ b/discord/Cargo.toml @@ -10,7 +10,9 @@ chrono = "0.4.42" clap = { version = "4.5.53", features = ["derive"] } dotenvy = "0.15.7" poise = "0.6.1" +rocket = { version = "0.5.1", features = ["json"] } serde = "1.0.228" -squad-quest = { version = "0.10.0", path = ".." } +serde_json = "1.0.146" +squad-quest = { version = "0.11.0", path = ".." } tokio = { version = "1.48.0", features = ["rt-multi-thread"] } toml = "0.9.8" diff --git a/discord/Rocket.toml b/discord/Rocket.toml new file mode 100644 index 0000000..034039b --- /dev/null +++ b/discord/Rocket.toml @@ -0,0 +1,8 @@ +[default] +address = "127.0.0.1" # should be local only because frontend runs on the same machine +port = 2526 +log_level = "critical" + +[default.shutdown] +ctrlc = false + diff --git a/discord/src/account.rs b/discord/src/account.rs index bc433e3..0551ec6 100644 --- a/discord/src/account.rs +++ b/discord/src/account.rs @@ -1,12 +1,24 @@ -use poise::serenity_prelude::UserId; +use std::collections::HashMap; + +use poise::serenity_prelude::{User, UserId}; use squad_quest::{account::Account, config::Config, map::Map}; -pub fn fetch_or_init_account(conf: &Config, id: String) -> Account { +pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> Account { let accounts = conf.load_accounts(); + let mut data: HashMap = HashMap::new(); + + if let Some(user) = user { + let avatar = user.avatar_url().unwrap_or("null".to_string()); + let name = user.display_name().to_string(); + data.insert("avatar".to_string(), avatar); + data.insert("name".to_string(), name); + } + match accounts.iter().find(|a| a.id == id) { Some(a) => a.clone(), None => Account { id, + data: Some(data), ..Default::default() }, } diff --git a/discord/src/api.rs b/discord/src/api.rs new file mode 100644 index 0000000..1594240 --- /dev/null +++ b/discord/src/api.rs @@ -0,0 +1,119 @@ +use rocket::{Build, Response, Rocket, State, http::{Header, hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN}, response::Responder, serde::json::Json}; +use serde::Serialize; +use squad_quest::{SquadObject, account::Account, config::Config, map::{Map, Room}}; + +struct RocketData { + pub config: Config, +} + +#[derive(Serialize)] +struct UserData { + pub id: String, + pub avatar: String, + pub name: String, +} + +#[derive(Serialize)] +struct RoomData { + pub id: u16, + pub value: u32, + pub name: String, + pub description: String, + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub markers: Vec, +} + +struct RoomDataResponse { + pub data: Vec +} + +impl From> for RoomDataResponse { + fn from(value: Vec) -> Self { + Self { + data: value, + } + } +} + +impl<'r> Responder<'r, 'static> for RoomDataResponse { + fn respond_to(self, request: &'r rocket::Request<'_>) -> rocket::response::Result<'static> { + Response::build_from(Json(&self.data).respond_to(request)?) + .header(Header::new(ACCESS_CONTROL_ALLOW_ORIGIN.as_str(), "http://localhost:5173")) + .ok() + } +} + +impl From<&Room> for RoomData { + fn from(value: &Room) -> Self { + let data = value.data.clone().unwrap_or_default(); + let keys = [ "x", "y", "w", "h" ]; + let mut values = [ 0f32, 0f32, 0f32, 0f32 ]; + let mut counter = 0usize; + for key in keys { + values[counter] = data.get(key).map_or(0f32, |v| v.parse::().unwrap_or_default()); + counter += 1; + } + RoomData { + id: value.id, + value: value.value, + name: value.name.clone(), + description: value.description.clone().unwrap_or(String::new()), + x: values[0], + y: values[1], + w: values[2], + h: values[3], + markers: Vec::new(), + } + } +} + +fn acc_filt_map(account: &Account, room_id: u16) -> Option { + if account.location == room_id { + let data = account.data.clone().unwrap_or_default(); + let keys = [ "avatar", "name" ]; + let empty = String::new(); + let mut values = [ &String::new(), &String::new() ]; + let mut counter = 0usize; + for key in keys { + values[counter] = data.get(key).unwrap_or(&empty); + counter += 1; + } + Some(UserData { + id: account.id.clone(), + avatar: values[0].clone(), + name: values[1].clone(), + }) + } else { None } +} + +#[get("/")] +fn index(rd: &State) -> RoomDataResponse { + let map_path = rd.config.full_map_path(); + let Ok(map) = Map::load(map_path) else { + return Vec::new().into(); + }; + let accounts = rd.config.load_accounts(); + + let rooms_vec: Vec = map.room.iter() + .map(|r| { + let mut rd = RoomData::from(r); + let markers = accounts.iter() + .filter_map(|a| acc_filt_map(a, r.id)) + .collect::>(); + rd.markers = markers; + rd + }) + .collect(); + rooms_vec.into() +} + +pub fn rocket(config: Config) -> Rocket { + rocket::build() + .mount("/", routes![index]) + .manage(RocketData{config}) +} + + diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index d47b906..2d5d6f3 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -142,7 +142,8 @@ pub async fn give( let mut accounts = config.load_accounts(); let user_id = format!("{}", ctx.author().id.get()); - let mut user_account = fetch_or_init_account(config, user_id); + + let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author())); let who_id = format!("{}", who.id.get()); let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else { diff --git a/discord/src/commands/answer.rs b/discord/src/commands/answer.rs index 1f6445e..523db8f 100644 --- a/discord/src/commands/answer.rs +++ b/discord/src/commands/answer.rs @@ -34,7 +34,7 @@ pub async fn answer( #[description_localized("ru", "Вложение к ответу на квест")] file3: Option, ) -> Result<(), Error> { - let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string()); + let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string(), Some(ctx.author())); if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) { return Err(Error::QuestIsCompleted(quest_id)); diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index 96bb30d..064595d 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -27,7 +27,7 @@ pub async fn unlock( }; let acc_id = format!("{}", ctx.author().id.get()); - let mut account = fetch_or_init_account(conf, acc_id); + let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); if account.balance < room.value { return Err(Error::InsufficientFunds(room.value)); @@ -68,7 +68,7 @@ pub async fn r#move( let conf = &ctx.data().config; let acc_id = format!("{}", ctx.author().id.get()); - let mut account = fetch_or_init_account(conf, acc_id); + let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) { return Err(Error::CannotReach(id)); diff --git a/discord/src/main.rs b/discord/src/main.rs index cfb68c7..ffe6922 100644 --- a/discord/src/main.rs +++ b/discord/src/main.rs @@ -1,3 +1,5 @@ +#[macro_use] extern crate rocket; + use std::{sync::{Arc, Mutex}}; use clap::Parser; @@ -5,8 +7,9 @@ use dotenvy::dotenv; use poise::serenity_prelude as serenity; use squad_quest::config::Config; -use crate::{commands::error_handler, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings}; +use crate::{commands::{error_handler, print_error_recursively}, config::{ConfigImpl, DiscordConfig}, error::Error, strings::Strings}; +mod api; mod commands; mod cli; mod config; @@ -65,6 +68,14 @@ async fn main() { let token = std::env::var(DISCORD_TOKEN).expect("missing DISCORD_TOKEN"); let intents = serenity::GatewayIntents::non_privileged(); + let conf1 = config.clone(); + tokio::spawn(async { + if let Err(error) = api::rocket(conf1).launch().await { + eprintln!("ERROR ON API LAUNCH"); + print_error_recursively(&error); + } + }); + let framework = poise::Framework::builder() .options(poise::FrameworkOptions { on_error: |err| Box::pin(error_handler(err)), @@ -87,6 +98,8 @@ async fn main() { .setup(|_ctx, _ready, _framework| { Box::pin(async move { //poise::builtins::register_globally(ctx, &framework.options().commands).await?; + + Ok(Data { config, discord: Arc::new(Mutex::new(discord)), diff --git a/src/account/mod.rs b/src/account/mod.rs index d816548..bfb6157 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -1,6 +1,6 @@ //! User accounts -use std::{fs, io::Write, path::PathBuf}; +use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use serde::{ Serialize, Deserialize }; @@ -29,6 +29,9 @@ pub struct Account { /// Vec of rooms unlocked by this user pub rooms_unlocked: Vec, + + /// Additional implementation-defined data + pub data: Option>, } impl Default for Account { @@ -39,6 +42,7 @@ impl Default for Account { location: u16::default(), quests_completed: Vec::new(), rooms_unlocked: Vec::new(), + data: None, } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 07aa1f1..97a2634 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{SquadObject, account::Account, error::Error, quest::Quest}; /// Struct for containing paths to other (de-)serializable things -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(default)] pub struct Config { /// Path to config directory diff --git a/src/map/mod.rs b/src/map/mod.rs index 1ed9aaf..819962a 100644 --- a/src/map/mod.rs +++ b/src/map/mod.rs @@ -1,6 +1,6 @@ //! Map, a.k.a. a graph of rooms -use std::{fs, io::Write, path::PathBuf}; +use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use serde::{Deserialize, Serialize}; @@ -11,7 +11,7 @@ use crate::{SquadObject, account::Account, error::{Error, MapError}}; #[serde(default)] pub struct Map { /// Rooms go here - pub room: Vec + pub room: Vec, } impl Default for Map { @@ -131,6 +131,8 @@ pub struct Room { pub name: String, /// Room description pub description: Option, + /// Additional implementation-based data + pub data: Option>, } fn default_name() -> String { @@ -145,6 +147,7 @@ impl Default for Room { value: u32::default(), name: default_name(), description: None, + data: None, } } } diff --git a/src/quest/mod.rs b/src/quest/mod.rs index 669d061..da28415 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -1,6 +1,6 @@ //! Text-based quests and user solutions for them -use std::{fs, io::Write, path::PathBuf}; +use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use serde::{ Serialize, Deserialize }; use crate::{SquadObject, account::Account, error::{Error, QuestError}}; @@ -66,7 +66,10 @@ pub struct Quest { pub available_on: Option, /// When quest expires - pub deadline: Option + pub deadline: Option, + + /// Additional implementation-defined data + pub data: Option>, } impl Default for Quest { @@ -80,7 +83,8 @@ impl Default for Quest { answer: default_answer(), public: false, available_on: None, - deadline: None + deadline: None, + data: None, } } } diff --git a/tests/main.rs b/tests/main.rs index b2d054d..73a63f5 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -38,7 +38,8 @@ fn quest_one() { answer: "Accept the answer if it has no attachments and an empty comment".to_owned(), public: false, available_on: None, - deadline: None + deadline: None, + ..Default::default() }; assert_eq!(*quest, expected); @@ -73,7 +74,8 @@ fn account_test() { balance: 150, location: 0, quests_completed: vec![0], - rooms_unlocked: Vec::new() + rooms_unlocked: Vec::new(), + ..Default::default() }; let accounts = config.load_accounts(); @@ -92,6 +94,7 @@ fn load_map() { value: 0, name: "Entrance".to_string(), description: Some("Enter the dungeon".to_string()), + ..Default::default() }; let room1 = Room { @@ -100,6 +103,7 @@ fn load_map() { value: 100, name: "Kitchen hall".to_string(), description: None, + ..Default::default() }; let room2 = Room { @@ -108,6 +112,7 @@ fn load_map() { value: 250, name: "Room".to_string(), description: Some("Simple room with no furniture".to_string()), + ..Default::default() }; let room3 = Room { @@ -116,6 +121,7 @@ fn load_map() { value: 175, name: "Kitchen".to_string(), description: Some("Knives are stored here".to_string()), + ..Default::default() }; let expected = Map { From d188bba16e581bb9dfc6b60c6e6bdd67f08b6c0f Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 24 Dec 2025 17:46:22 +0300 Subject: [PATCH 7/9] feat: Implemented guild check - Also added more error logging --- discord/src/commands/account.rs | 7 ++++++- discord/src/commands/answer.rs | 3 ++- discord/src/commands/init.rs | 4 +++- discord/src/commands/map.rs | 4 +++- discord/src/commands/mod.rs | 23 +++++++++++++++++++++-- discord/src/commands/quest.rs | 8 +++++++- discord/src/commands/social.rs | 6 +++++- discord/src/error.rs | 5 ++++- 8 files changed, 51 insertions(+), 9 deletions(-) diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index 2d5d6f3..2b81750 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -1,7 +1,7 @@ use poise::serenity_prelude::User; use squad_quest::{SquadObject, account::Account, map::Map}; -use crate::{Context, Error, account::{account_full_balance, account_user_id, fetch_or_init_account}, strings::StringFormatter}; +use crate::{Context, Error, account::{account_full_balance, account_user_id, fetch_or_init_account}, strings::StringFormatter, commands::guild}; async fn account_balance_string( ctx: &Context<'_>, @@ -26,6 +26,7 @@ async fn account_balance_string( prefix_command, slash_command, guild_only, + check = "guild", required_permissions = "ADMINISTRATOR", name_localized("ru", "сбросить"), description_localized("ru", "Сбросить аккаунт пользователя, вкл. баланс, открытые комнаты и пройденные квесты"), @@ -63,6 +64,7 @@ pub async fn reset( prefix_command, slash_command, guild_only, + check = "guild", name_localized("ru", "счет"), description_localized("ru", "Отобразить таблицу лидеров"), )] @@ -106,6 +108,7 @@ pub async fn scoreboard( prefix_command, slash_command, guild_only, + check = "guild", subcommands("give", "set"), name_localized("ru", "баланс"), )] @@ -120,6 +123,7 @@ pub async fn balance( prefix_command, slash_command, guild_only, + check = "guild", name_localized("ru", "передать"), description_localized("ru", "Передать очки другому пользователю"), )] @@ -178,6 +182,7 @@ pub async fn give( prefix_command, slash_command, guild_only, + check = "guild", required_permissions = "ADMINISTRATOR", name_localized("ru", "установить"), description_localized("ru", "Устанавливает текущий баланс пользователя"), diff --git a/discord/src/commands/answer.rs b/discord/src/commands/answer.rs index 523db8f..2dd280a 100644 --- a/discord/src/commands/answer.rs +++ b/discord/src/commands/answer.rs @@ -1,13 +1,14 @@ use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage}; use squad_quest::SquadObject; -use crate::{Context, Error, account::fetch_or_init_account}; +use crate::{Context, Error, account::fetch_or_init_account, commands::guild}; /// Send an answer to the quest for review #[poise::command( prefix_command, slash_command, guild_only, + check = "guild", name_localized("ru", "ответить"), description_localized("ru", "Отправить ответ на квест на проверку"), )] diff --git a/discord/src/commands/init.rs b/discord/src/commands/init.rs index fb6e6c3..b4fd02e 100644 --- a/discord/src/commands/init.rs +++ b/discord/src/commands/init.rs @@ -4,7 +4,7 @@ use poise::{CreateReply, serenity_prelude::ChannelId}; use squad_quest::SquadObject; use toml::value::Time; -use crate::{Context, Error, timer::DailyTimer}; +use crate::{Context, Error, timer::DailyTimer, commands::guild}; /// Set channels to post quests and answers to #[poise::command( @@ -12,6 +12,7 @@ use crate::{Context, Error, timer::DailyTimer}; slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "инит"), description_localized("ru", "Установить каналы для публикации квестов и ответов"), )] @@ -75,6 +76,7 @@ fn seconds(time: Time) -> u64 { slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "таймер"), description_localized("ru", "Включить таймер публикации по заданной временной метке UTC (МСК -3)"), )] diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index 064595d..e22f2e4 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -1,12 +1,13 @@ use squad_quest::{SquadObject, map::Map}; -use crate::{Context, account::fetch_or_init_account, error::Error}; +use crate::{Context, account::fetch_or_init_account, error::Error, commands::guild}; /// Unlock specified room if it is reachable and you have required amount of points #[poise::command( prefix_command, slash_command, guild_only, + check = "guild", name_localized("ru", "открыть"), description_localized("ru", "Открывает указанную комнату, если хватает очков и до нее можно добраться"), )] @@ -55,6 +56,7 @@ pub async fn unlock( prefix_command, slash_command, guild_only, + check = "guild", name_localized("ru", "пойти"), description_localized("ru", "Переместиться в другую разблокированную комнату"), )] diff --git a/discord/src/commands/mod.rs b/discord/src/commands/mod.rs index 85d3ee8..b274168 100644 --- a/discord/src/commands/mod.rs +++ b/discord/src/commands/mod.rs @@ -1,5 +1,5 @@ use std::error::Error as StdError; -use poise::CreateReply; +use poise::{CreateReply, serenity_prelude::GuildId}; use crate::{Context, Data, Error}; @@ -10,6 +10,18 @@ pub mod social; pub mod account; pub mod map; +pub async fn guild(ctx: Context<'_>) -> Result { + let id = ctx.guild_id().expect("guild-only command"); + let guard = ctx.data().discord.lock().expect("shouldn't be locked"); + let expected_id = guard.guild; + + if expected_id != GuildId::default() && id != expected_id { + return Err(Error::NotThisGuild); + } + + Ok(true) +} + #[poise::command(prefix_command)] pub async fn register(ctx: Context<'_>) -> Result<(), Error> { poise::builtins::register_application_commands_buttons(ctx).await?; @@ -36,10 +48,17 @@ pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) { eprintln!("ERROR:"); print_error_recursively(&error); if let Some(ctx) = error.ctx() { + let user = ctx.author().display_name(); + eprintln!("User: {user} ({id})", id = ctx.author().id); + let response = match error { - poise::FrameworkError::Command { error, .. } => format!("Internal server error: {error}"), + poise::FrameworkError::Command { error, .. } => { + eprintln!("Invokation string: {}", ctx.invocation_string()); + format!("Internal server error: {error}") + }, _ => format!("Internal server error: {error}"), }; + if let Err(error) = ctx.send(CreateReply::default().content(response).ephemeral(true)).await { eprintln!("Couldn't send error message: {error}"); } diff --git a/discord/src/commands/quest.rs b/discord/src/commands/quest.rs index 1d73544..489d70c 100644 --- a/discord/src/commands/quest.rs +++ b/discord/src/commands/quest.rs @@ -3,7 +3,7 @@ use std::{future, str::FromStr}; use poise::serenity_prelude::{CreateMessage, EditMessage, Message, futures::StreamExt}; use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}}; use toml::value::Date; -use crate::{Context, Error}; +use crate::{Context, Error,commands::guild}; async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result, Error>{ ctx.defer().await?; @@ -34,6 +34,7 @@ fn make_quest_message_content(ctx: Context<'_>, quest: &Quest) -> String { prefix_command, slash_command, guild_only, + check = "guild", subcommands("list", "create", "update", "publish", "delete"), required_permissions = "ADMINISTRATOR", name_localized("ru", "квест"), @@ -49,6 +50,7 @@ pub async fn quest( prefix_command, slash_command, guild_only, + check = "guild", required_permissions = "ADMINISTRATOR", name_localized("ru", "список"), description_localized("ru", "Вывести все квесты") @@ -114,6 +116,7 @@ impl From for Date { slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "создать"), description_localized("ru", "Создать квест и получить его идентификатор"), )] @@ -202,6 +205,7 @@ pub async fn create( slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "обновить"), description_localized("ru", "Обновить выбранные значения указанного квеста"), )] @@ -339,6 +343,7 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result ) -> Result<(), Error> { slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "написать"), description_localized("ru", "Отправить сообщение пользователю или в канал"), )] @@ -98,6 +100,7 @@ pub async fn msg ( slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "редактировать"), description_localized("ru", "Редактировать сообщение в канале или в ЛС"), )] @@ -177,6 +180,7 @@ pub async fn edit ( slash_command, required_permissions = "ADMINISTRATOR", guild_only, + check = "guild", name_localized("ru", "удалить"), description_localized("ru", "Удалить сообщение в канале или в ЛС"), )] diff --git a/discord/src/error.rs b/discord/src/error.rs index 8d8b442..fe80e0d 100644 --- a/discord/src/error.rs +++ b/discord/src/error.rs @@ -21,6 +21,7 @@ pub enum Error { RoomAlreadyUnlocked(u16), CannotReach(u16), TimerSet, + NotThisGuild, } impl From for Error { @@ -65,6 +66,7 @@ impl Display for Error { Self::RoomAlreadyUnlocked(id) => write!(f, "room #{id} is already unlocked for this user"), Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"), Self::TimerSet => write!(f, "timer is already set"), + Self::NotThisGuild => write!(f, "cannot be used in this guild"), } } } @@ -84,7 +86,8 @@ impl std::error::Error for Error { Self::RoomNotFound(_) | Self::RoomAlreadyUnlocked(_) | Self::CannotReach(_) | - Self::TimerSet => None, + Self::TimerSet | + Self::NotThisGuild => None, Self::SerenityError(error) => Some(error), Self::SquadQuestError(error) => Some(error), } From 2640821a05f5b6efd0af9ebb9de1e8c3832b8a40 Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Tue, 30 Dec 2025 15:44:23 +0300 Subject: [PATCH 8/9] feat!: Added limit field to quests - Bump version to 0.12.0 - lib: Changed Quest::complete_for_account behavior - cli: Added limit field for quest create and quest update - discord: Quests are checked for limit on /answer - discord: Added limit field for /quest create and /quest update - discord: Changed behavior of fetch_or_init_account --- Cargo.lock | 6 ++-- Cargo.toml | 2 +- cli/Cargo.toml | 2 +- cli/src/cli/quest.rs | 6 ++++ cli/src/main.rs | 18 ++++++---- discord/Cargo.toml | 2 +- discord/src/account.rs | 23 +++++++----- discord/src/commands/account.rs | 46 ++++++++++++++---------- discord/src/commands/answer.rs | 35 +++++++++++++----- discord/src/commands/map.rs | 10 ++++-- discord/src/commands/quest.rs | 63 ++++++++++++++++++++++++--------- discord/src/error.rs | 5 ++- discord/src/strings.rs | 11 +++++- src/account/mod.rs | 9 ++++- src/error.rs | 6 ++++ src/quest/mod.rs | 17 ++++++++- 16 files changed, 192 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 388a656..1be0a5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2140,7 +2140,7 @@ checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "squad-quest" -version = "0.11.0" +version = "0.12.0" dependencies = [ "serde", "toml 0.9.10+spec-1.1.0", @@ -2148,7 +2148,7 @@ dependencies = [ [[package]] name = "squad-quest-cli" -version = "0.11.0" +version = "0.12.0" dependencies = [ "chrono", "clap", @@ -2159,7 +2159,7 @@ dependencies = [ [[package]] name = "squad-quest-discord" -version = "0.11.0" +version = "0.12.0" dependencies = [ "chrono", "clap", diff --git a/Cargo.toml b/Cargo.toml index bffc69b..7cc9deb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ members = ["cli", "discord"] [workspace.package] -version = "0.11.0" +version = "0.12.0" edition = "2024" repository = "https://2ndbeam.ru/git/2ndbeam/squad-quest" homepage = "https://2ndbeam.ru/git/2ndbeam/squad-quest" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ac8581c..9585de5 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.11.0", path = ".." } +squad-quest = { version = "0.12.0", path = ".." } toml = "0.9.8" diff --git a/cli/src/cli/quest.rs b/cli/src/cli/quest.rs index 1ed541c..41c5a7e 100644 --- a/cli/src/cli/quest.rs +++ b/cli/src/cli/quest.rs @@ -83,6 +83,9 @@ pub struct QuestCreateArgs { /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) #[arg(short,long,value_parser = parse_date)] pub deadline: Option, + /// Limit on how many users can solve the quest (0 = no limit) + #[arg(short,long)] + pub limit: Option, } #[derive(Args)] @@ -113,6 +116,9 @@ pub struct QuestUpdateArgs { /// Quest expiration date (format = YYYY-MM-DD, ex. 2025-12-24) #[arg(long,value_parser = parse_date)] pub deadline: Option, + /// Limit on how many users can solve the quest (0 = no limit) + #[arg(long)] + pub limit: Option, } #[derive(Args)] diff --git a/cli/src/main.rs b/cli/src/main.rs index eb6dceb..1a022e9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -148,6 +148,7 @@ fn main() { public: args.public, available_on: args.available.clone(), deadline: args.deadline.clone(), + limit: args.limit.unwrap_or_default(), ..Default::default() }; @@ -171,6 +172,7 @@ fn main() { public: args.public.unwrap_or(quest.public), available_on: args.available.clone().or(quest.available_on.clone()), deadline: args.deadline.clone().or(quest.deadline.clone()), + limit: args.limit.unwrap_or_default(), ..Default::default() }; @@ -284,10 +286,6 @@ fn main() { do_and_log(account.save(path), !cli.quiet, format!("Updated balance of account \"{}\".", account.id)); }, AccountCommands::Complete(args) => { - let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else { - if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); } - return; - }; let quests = config.load_quests(); @@ -299,9 +297,17 @@ fn main() { }, }; - match quest.complete_for_account(account) { + let result = quest.complete_for_account(&args.account, &mut accounts); + + match result { Err(error) if !cli.quiet => println!("Error: {error}"), - Ok(_) => do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)), + Ok(_) => { + let Some(account) = accounts.iter_mut().find(|a| a.id == args.account) else { + if !cli.quiet { eprintln!("Error: account \"{}\" not found.", args.account); } + return; + }; + do_and_log(account.save(path), !cli.quiet, format!("Completed quest #{} on account \"{}\".", args.quest, account.id)); + }, _ => {}, } }, diff --git a/discord/Cargo.toml b/discord/Cargo.toml index 510a366..384d709 100644 --- a/discord/Cargo.toml +++ b/discord/Cargo.toml @@ -13,6 +13,6 @@ poise = "0.6.1" rocket = { version = "0.5.1", features = ["json"] } serde = "1.0.228" serde_json = "1.0.146" -squad-quest = { version = "0.11.0", path = ".." } +squad-quest = { version = "0.12.0", path = ".." } tokio = { version = "1.48.0", features = ["rt-multi-thread"] } toml = "0.9.8" diff --git a/discord/src/account.rs b/discord/src/account.rs index 0551ec6..8950210 100644 --- a/discord/src/account.rs +++ b/discord/src/account.rs @@ -3,8 +3,14 @@ use std::collections::HashMap; use poise::serenity_prelude::{User, UserId}; use squad_quest::{account::Account, config::Config, map::Map}; -pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> Account { +/// Returns Ok(account) if account was found or Err(new_account) if not +pub fn fetch_or_init_account(conf: &Config, id: &str, user: Option<&User>) -> Result { let accounts = conf.load_accounts(); + + if let Some(account) = accounts.iter().find(|a| a.id == id) { + return Ok(account.clone()); + } + let mut data: HashMap = HashMap::new(); if let Some(user) = user { @@ -14,14 +20,13 @@ pub fn fetch_or_init_account(conf: &Config, id: String, user: Option<&User>) -> data.insert("name".to_string(), name); } - match accounts.iter().find(|a| a.id == id) { - Some(a) => a.clone(), - None => Account { - id, - data: Some(data), - ..Default::default() - }, - } + let new_account = Account { + id: id.to_string(), + data: Some(data), + ..Default::default() + }; + + Err(new_account) } pub fn account_rooms_value(account: &Account, map: &Map) -> u32 { diff --git a/discord/src/commands/account.rs b/discord/src/commands/account.rs index 2b81750..91ec72d 100644 --- a/discord/src/commands/account.rs +++ b/discord/src/commands/account.rs @@ -1,7 +1,7 @@ use poise::serenity_prelude::User; use squad_quest::{SquadObject, account::Account, map::Map}; -use crate::{Context, Error, account::{account_full_balance, account_user_id, fetch_or_init_account}, strings::StringFormatter, commands::guild}; +use crate::{Context, Error, account::{account_full_balance, account_user_id}, strings::StringFormatter, commands::guild}; async fn account_balance_string( ctx: &Context<'_>, @@ -146,31 +146,41 @@ pub async fn give( let mut accounts = config.load_accounts(); let user_id = format!("{}", ctx.author().id.get()); - - let mut user_account = fetch_or_init_account(config, user_id, Some(ctx.author())); - + let strings = &ctx.data().strings; + let formatter: StringFormatter; + let accounts_path = config.full_accounts_path(); + let who_id = format!("{}", who.id.get()); - let Some(other_account) = accounts.iter_mut().find(|a| a.id == who_id ) else { - return Err(Error::AccountNotFound); - }; - if user_account.balance < amount { - return Err(Error::InsufficientFunds(amount)); + if let None = accounts.iter().find(|a| a.id == who_id ) { + return Err(Error::AccountNotFound); } - user_account.balance -= amount; + { + let Some(user_account) = accounts.iter_mut().find(|a| a.id == user_id) else { + return Err(Error::AccountNotFound); + }; + + if user_account.balance < amount { + return Err(Error::InsufficientFunds(amount)); + } + + user_account.balance -= amount; + user_account.save(accounts_path.clone())?; + + formatter = strings.formatter() + .value(amount) + .user(&who) + .current_balance(&user_account); + } + + let other_account = accounts.iter_mut().find(|a| a.id == who_id ).expect("We already checked its existence earlier"); + other_account.balance += amount; - let accounts_path = config.full_accounts_path(); - user_account.save(accounts_path.clone())?; other_account.save(accounts_path)?; - let strings = &ctx.data().strings; - let formatter = strings.formatter() - .user(&who) - .value(amount) - .current_balance(&user_account); - + let reply_string = formatter.fmt(&strings.account.give_pt); ctx.reply(reply_string).await?; diff --git a/discord/src/commands/answer.rs b/discord/src/commands/answer.rs index 2dd280a..964ea6a 100644 --- a/discord/src/commands/answer.rs +++ b/discord/src/commands/answer.rs @@ -1,7 +1,7 @@ use poise::serenity_prelude::{Attachment, ComponentInteractionCollector, CreateActionRow, CreateAttachment, CreateButton, CreateMessage, EditMessage}; use squad_quest::SquadObject; -use crate::{Context, Error, account::fetch_or_init_account, commands::guild}; +use crate::{account::fetch_or_init_account, commands::{guild, quest::update_quest_message}, Context, Error}; /// Send an answer to the quest for review #[poise::command( @@ -35,19 +35,34 @@ pub async fn answer( #[description_localized("ru", "Вложение к ответу на квест")] file3: Option, ) -> Result<(), Error> { - let mut account = fetch_or_init_account(&ctx.data().config, ctx.author().id.to_string(), Some(ctx.author())); - - if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) { - return Err(Error::QuestIsCompleted(quest_id)); - } - let quests = ctx.data().config.load_quests(); let Some(quest) = quests.iter() .filter(|q| q.public) .find(|q| q.id == quest_id) else { return Err(Error::QuestNotFound(quest_id)); }; + { + let accounts = ctx.data().config.load_accounts(); + let completed_times = accounts.iter().filter(|a| a.has_completed_quest(&quest)).count() as u8; + if quest.limit > 0 && completed_times >= quest.limit { + return Err(Error::QuestLimitExceeded(quest.id)); + } + } + let user_id = ctx.author().id.to_string(); + match fetch_or_init_account(&ctx.data().config, &user_id, Some(ctx.author())) { + Ok(account) => { + if let Some(_) = account.quests_completed.iter().find(|qid| **qid == quest_id) { + return Err(Error::QuestIsCompleted(quest_id)); + } + }, + Err(new_account) => { + let path = ctx.data().config.full_accounts_path(); + new_account.save(path)? + } + } + + let mut files: Vec = Vec::new(); for file in [file1, file2, file3] { if let Some(f) = file { @@ -135,16 +150,20 @@ pub async fn answer( let content: String; if is_approved { let mut no_errors = true; - if let Err(error) = quest.complete_for_account(&mut account) { + let mut accounts = ctx.data().config.load_accounts(); + if let Err(error) = quest.complete_for_account(&ctx.author().id.to_string(), &mut accounts) { eprintln!("{error}"); no_errors = false; }; + let account = accounts.iter_mut().find(|a| a.id == user_id).expect("we done fetch_or_init earlier"); let path = ctx.data().config.full_accounts_path(); if let Err(error) = account.save(path) { eprintln!("{error}"); no_errors = false; }; + update_quest_message(ctx, &quest).await?; + formatter = formatter.current_balance(&account); if no_errors { diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index e22f2e4..e23134d 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -28,7 +28,10 @@ pub async fn unlock( }; let acc_id = format!("{}", ctx.author().id.get()); - let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); + let mut account = match fetch_or_init_account(conf, &acc_id, Some(ctx.author())) { + Ok(account) => account, + Err(account) => account, + }; if account.balance < room.value { return Err(Error::InsufficientFunds(room.value)); @@ -70,7 +73,10 @@ pub async fn r#move( let conf = &ctx.data().config; let acc_id = format!("{}", ctx.author().id.get()); - let mut account = fetch_or_init_account(conf, acc_id, Some(ctx.author())); + let mut account = match fetch_or_init_account(conf, &acc_id, Some(ctx.author())) { + Ok(account) => account, + Err(account) => account, + }; if let None = account.rooms_unlocked.iter().find(|rid| **rid == id) { return Err(Error::CannotReach(id)); diff --git a/discord/src/commands/quest.rs b/discord/src/commands/quest.rs index 489d70c..a04b0ef 100644 --- a/discord/src/commands/quest.rs +++ b/discord/src/commands/quest.rs @@ -1,7 +1,8 @@ use std::{future, str::FromStr}; -use poise::serenity_prelude::{CreateMessage, EditMessage, Message, futures::StreamExt}; -use squad_quest::{SquadObject, quest::{Quest, QuestDifficulty}}; +use poise::serenity_prelude as serenity; +use serenity::{CreateMessage, EditMessage, Message, futures::StreamExt}; +use squad_quest::{account::Account, quest::{Quest, QuestDifficulty}, SquadObject}; use toml::value::Date; use crate::{Context, Error,commands::guild}; @@ -24,12 +25,37 @@ async fn find_quest_message(ctx: Context<'_>, id: u16) -> Result Ok(messages.first().cloned()) } -fn make_quest_message_content(ctx: Context<'_>, quest: &Quest) -> String { +fn make_quest_message_content(ctx: Context<'_>, quest: &Quest, accounts: &Option>) -> String { let strings = &ctx.data().strings; - let formatter = strings.formatter().quest(quest); + let formatter = match accounts { + Some(accounts) => strings.formatter().quest_full(quest, accounts), + None => strings.formatter().quest(quest), + }; formatter.fmt(&strings.quest.message_format) } +pub async fn update_quest_message(ctx: Context<'_>, quest: &Quest) -> Result<(), Error> { + let strings = &ctx.data().strings; + let formatter = strings.formatter().quest(&quest); + let accounts = ctx.data().config.load_accounts(); + let content = make_quest_message_content(ctx, &quest, &Some(accounts)); + let builder = EditMessage::new().content(content); + + let message = find_quest_message(ctx, quest.id).await?; + if let Some(mut message) = message { + return match message.edit(ctx, builder).await { + Ok(_) => Ok(()), + Err(error) => Err(error.into()), + } + } else { + let reply_string = formatter.fmt(&strings.quest.message_not_found); + match ctx.reply(reply_string).await { + Ok(_) => Ok(()), + Err(error) => Err(error.into()), + } + } +} + #[poise::command( prefix_command, slash_command, @@ -146,6 +172,10 @@ pub async fn create( #[name_localized("ru", "доступен")] #[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24)")] available: Option, + #[description = "Limit how many users are allowed to complete the quest"] + #[name_localized("ru", "лимит")] + #[description_localized("ru", "Ограничение количества участников, которые могут выполнить квест")] + limit: Option, /* #[description = "Deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "дедлайн")] @@ -182,6 +212,7 @@ pub async fn create( answer, public: false, available_on, + limit: limit.unwrap_or_default(), //deadline, ..Default::default() }; @@ -239,6 +270,10 @@ pub async fn update( #[name_localized("ru", "доступен")] #[description_localized("ru", "Дата публикации (в формате ГГГГ-ММ-ДД, напр. 2025-12-24")] available: Option, + #[description = "Limit how many users are allowed to complete the quest"] + #[name_localized("ru", "лимит")] + #[description_localized("ru", "Ограничение количества участников, которые могут выполнить квест")] + limit: Option, /* #[description = "Quest deadline (in format of YYYY-MM-DD, e.g. 2025-12-24)"] #[name_localized("ru", "дедлайн")] @@ -264,15 +299,18 @@ pub async fn update( }; let available_on: Option; + let new_limit: u8; //let dead_line: Option; match reset.unwrap_or(false) { true => { available_on = None; + new_limit = limit.unwrap_or_default(); //dead_line = None; }, false => { available_on = available.map_or_else(|| quest.available_on.clone(), |v| Some(v.into())); + new_limit = limit.unwrap_or(quest.limit); //dead_line = deadline.map_or_else(|| quest.deadline.clone(), |v| Some(v.into())); }, } @@ -286,6 +324,7 @@ pub async fn update( answer: answer.unwrap_or(quest.answer.clone()), public: quest.public, available_on, + limit: new_limit, //deadline: dead_line, ..Default::default() }; @@ -294,16 +333,7 @@ pub async fn update( let formatter = strings.formatter().quest(&new_quest); if new_quest.public { - let content = make_quest_message_content(ctx, &new_quest); - let builder = EditMessage::new().content(content); - - let message = find_quest_message(ctx, id).await?; - if let Some(mut message) = message { - message.edit(ctx, builder).await?; - } else { - let reply_string = formatter.fmt(&strings.quest.message_not_found); - ctx.reply(reply_string).await?; - } + update_quest_message(ctx, &new_quest).await?; } let path = conf.full_quests_path(); @@ -319,8 +349,9 @@ pub async fn publish_inner(ctx: Context<'_>, quest: &mut Quest) -> Result for Error { @@ -67,6 +68,7 @@ impl Display for Error { Self::CannotReach(id) => write!(f, "user cannot reach room #{id}"), Self::TimerSet => write!(f, "timer is already set"), Self::NotThisGuild => write!(f, "cannot be used in this guild"), + Self::QuestLimitExceeded(id) => write!(f, "exceeded limit for quest #{id}"), } } } @@ -87,7 +89,8 @@ impl std::error::Error for Error { Self::RoomAlreadyUnlocked(_) | Self::CannotReach(_) | Self::TimerSet | - Self::NotThisGuild => None, + Self::NotThisGuild | + Self::QuestLimitExceeded(_) => None, Self::SerenityError(error) => Some(error), Self::SquadQuestError(error) => Some(error), } diff --git a/discord/src/strings.rs b/discord/src/strings.rs index afa2ac8..43505d4 100644 --- a/discord/src/strings.rs +++ b/discord/src/strings.rs @@ -45,11 +45,20 @@ impl StringFormatter { let name = ("{q.name}".to_string(), quest.name.clone()); let description = ("{q.description}".to_string(), quest.description.clone()); let answer = ("{q.answer}".to_string(), quest.answer.clone()); - let new_tags = vec![ id, difficulty, reward, name, description, answer ]; + let limit = ("{q.limit}".to_string(), quest.limit.to_string()); + let new_tags = vec![ id, difficulty, reward, name, description, answer, limit ]; self.with_tags(new_tags) } + pub fn quest_full(mut self, quest: &Quest, accounts: &Vec) -> Self { + self = self.quest(&quest); + let completed_times = accounts.iter().filter(|a| a.has_completed_quest(&quest)).count(); + let completions = ("{q.completions}".to_string(), completed_times.to_string()); + + self.with_tags(vec![completions]) + } + pub fn user(self, user: &User) -> Self { let mention = ("{u.mention}".to_string(), user.mention().to_string()); let name = ("{u.name}".to_string(), user.display_name().to_string()); diff --git a/src/account/mod.rs b/src/account/mod.rs index bfb6157..2d8b296 100644 --- a/src/account/mod.rs +++ b/src/account/mod.rs @@ -4,7 +4,7 @@ use std::{collections::HashMap, fs, io::Write, path::PathBuf}; use serde::{ Serialize, Deserialize }; -use crate::{SquadObject, error::Error}; +use crate::{error::Error, quest::Quest, SquadObject}; fn default_id() -> String { "none".to_string() @@ -99,3 +99,10 @@ impl SquadObject for Account { Ok(()) } } + +impl Account { + /// Returns true if given quest is completed on this account + pub fn has_completed_quest(&self, quest: &Quest) -> bool { + self.quests_completed.contains(&quest.id) + } +} diff --git a/src/error.rs b/src/error.rs index f4f1bc3..6bf532a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,12 +49,18 @@ impl std::error::Error for Error { pub enum QuestError { /// Quest (self.0) is already completed for given account (self.1) AlreadyCompleted(u16, String), + /// Account (self.0) not found + AccountNotFound(String), + /// Limit for quest (self.0) exceeded + LimitExceeded(u16), } impl fmt::Display for QuestError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::AlreadyCompleted(quest_id, account_id) => write!(f, "quest #{quest_id} is already completed for account \"{account_id}\""), + Self::AccountNotFound(account_id) => write!(f, "account \"{account_id}\""), + Self::LimitExceeded(quest_id) => write!(f, "exceeded limit for quest #{quest_id}"), } } } diff --git a/src/quest/mod.rs b/src/quest/mod.rs index da28415..dc6c7ce 100644 --- a/src/quest/mod.rs +++ b/src/quest/mod.rs @@ -70,6 +70,10 @@ pub struct Quest { /// Additional implementation-defined data pub data: Option>, + + /// Limit how many users can complete the quest. + /// If set to 0, quest is not limited. + pub limit: u8, } impl Default for Quest { @@ -85,6 +89,7 @@ impl Default for Quest { available_on: None, deadline: None, data: None, + limit: 0, } } } @@ -159,7 +164,17 @@ impl Quest { /// // handle error /// } /// ``` - pub fn complete_for_account(&self, account: &mut Account) -> Result<(),QuestError> { + pub fn complete_for_account(&self, id: &str, accounts: &mut Vec) -> Result<(),QuestError> { + let completed_times = accounts.iter().filter(|a| a.has_completed_quest(self)).count(); + + if self.limit > 0 && completed_times as u8 >= self.limit { + return Err(QuestError::LimitExceeded(self.id)); + } + + let Some(account) = accounts.iter_mut().find(|a| a.id == id) else { + return Err(QuestError::AccountNotFound(id.to_string())); + }; + match account.quests_completed.iter().find(|qid| **qid == self.id) { Some(_) => Err(QuestError::AlreadyCompleted(self.id, account.id.clone())), None => { From 1db7ce877ef99dac0be3ea6c7f1f63f7afb89dbd Mon Sep 17 00:00:00 2001 From: 2ndbeam <2ndbeam@disroot.org> Date: Wed, 31 Dec 2025 23:31:41 +0300 Subject: [PATCH 9/9] feat(discord): Added /avatar command - Added formattable error strings --- discord/src/commands/map.rs | 97 +++++++++++++++++++++++++++++++++++++ discord/src/commands/mod.rs | 45 ++++++++++++++--- discord/src/error.rs | 46 ++++++++++++++++-- discord/src/main.rs | 1 + discord/src/strings.rs | 60 +++++++++++++++++++++++ 5 files changed, 236 insertions(+), 13 deletions(-) diff --git a/discord/src/commands/map.rs b/discord/src/commands/map.rs index e23134d..0b6dfb5 100644 --- a/discord/src/commands/map.rs +++ b/discord/src/commands/map.rs @@ -1,3 +1,4 @@ +use poise::{serenity_prelude::{Attachment, CreateAttachment, CreateMessage}, CreateReply}; use squad_quest::{SquadObject, map::Map}; use crate::{Context, account::fetch_or_init_account, error::Error, commands::guild}; @@ -96,3 +97,99 @@ pub async fn r#move( Ok(()) } + +/// Change avatar on web map +#[poise::command( + slash_command, + prefix_command, + guild_only, + check = "guild", + name_localized("ru", "аватар"), + description_localized("ru", "Сменить аватар на веб карте"), +)] +pub async fn avatar( + ctx: Context<'_>, + #[description = "URL to the avatar"] + #[name_localized("ru", "ссылка")] + #[description_localized("ru", "Ссылка на аватар")] + url: Option, + #[description = "Attachment to use as avatar"] + #[name_localized("ru", "вложение")] + #[description_localized("ru", "Вложение, используемое как аватар")] + attachment: Option, +) -> Result<(), Error> { + let user_id = ctx.author().id.to_string(); + let mut accounts = ctx.data().config.load_accounts(); + let Some(account) = accounts.iter_mut().find(|a| a.id == user_id) else { + return Err(Error::AccountNotFound); + }; + + if url.is_none() && attachment.is_none() { + return Err(Error::NoUrlOrAttachment); + } + + if url.is_some() && attachment.is_some() { + return Err(Error::BothUrlAndAttachment); + } + + let strings = &ctx.data().strings; + let formatter = strings.formatter(); + + if let Some(url) = url { + let attachment = CreateAttachment::url(ctx, &url).await?; + let reply_string = formatter.fmt(&strings.map.processing_url); + let builder = CreateMessage::new() + .content(reply_string) + .add_file(attachment.clone()); + let message = ctx.channel_id().send_message(ctx, builder).await?; + + let attachment_check = message.attachments.first().expect("we just sent it"); + if attachment_check.width.is_none() + || !attachment_check.content_type + .as_ref() + .is_some_and(|t| t.starts_with("image/")) { + message.delete(ctx).await?; + return Err(Error::NonImageAttachment); + } + let data = account.data.as_mut().expect("automatically created"); + data.insert("avatar".to_string(), url); + + message.delete(ctx).await?; + + let reply_string = formatter.fmt(&strings.map.updated_avatar); + let builder = CreateReply::default() + .content(reply_string) + .attachment(attachment) + .reply(true); + ctx.send(builder).await?; + + } else if let Some(attachment) = attachment { + if attachment.width.is_none() + || !attachment.content_type + .as_ref() + .is_some_and(|t| t.starts_with("image/")) { + return Err(Error::NonImageAttachment); + } + + let reply_string = formatter.fmt(&strings.map.updated_avatar); + let copied_attachment = CreateAttachment::url(ctx, &attachment.url).await?; + + let data = account.data.as_mut().expect("automatically created"); + data.insert("avatar".to_string(), attachment.url); + + let path = ctx.data().config.full_accounts_path(); + account.save(path)?; + + let builder = CreateReply::default() + .content(reply_string) + .attachment(copied_attachment) + .reply(true); + + ctx.send(builder).await?; + } + + let path = ctx.data().config.full_accounts_path(); + account.save(path)?; + + Ok(()) +} diff --git a/discord/src/commands/mod.rs b/discord/src/commands/mod.rs index b274168..bba7684 100644 --- a/discord/src/commands/mod.rs +++ b/discord/src/commands/mod.rs @@ -1,7 +1,8 @@ use std::error::Error as StdError; use poise::{CreateReply, serenity_prelude::GuildId}; +use squad_quest::quest::Quest; -use crate::{Context, Data, Error}; +use crate::{error, Context, Data, Error}; pub mod quest; pub mod init; @@ -49,14 +50,42 @@ pub async fn error_handler(error: poise::FrameworkError<'_, Data, Error>) { print_error_recursively(&error); if let Some(ctx) = error.ctx() { let user = ctx.author().display_name(); - eprintln!("User: {user} ({id})", id = ctx.author().id); - let response = match error { - poise::FrameworkError::Command { error, .. } => { - eprintln!("Invokation string: {}", ctx.invocation_string()); - format!("Internal server error: {error}") - }, - _ => format!("Internal server error: {error}"), + eprintln!("User: {user} ({id})", id = ctx.author().id); + eprintln!("Invokation string: {}", ctx.invocation_string()); + + let strings = &ctx.data().strings; + + let response = if let poise::FrameworkError::Command { error, .. } = error { + let formatter = match error { + error::Error::QuestNotFound(id) | + error::Error::QuestIsPublic(id) | + error::Error::QuestIsCompleted(id) | + error::Error::QuestLimitExceeded(id) => + strings.formatter().quest(&Quest { id, ..Default::default() }), + + error::Error::InsufficientFunds(amount) => + strings.formatter().value(amount), + + error::Error::RoomNotFound(value) | + error::Error::RoomAlreadyUnlocked(value) | + error::Error::CannotReach(value) => + strings.formatter().value(value), + + error::Error::SerenityError(ref error) => + strings.formatter().text(error), + + error::Error::SquadQuestError(ref error) => + strings.formatter().text(error), + + _ => strings.formatter(), + }; + + let error_string = error.formattable_string(&strings.error); + formatter.fmt(error_string) + } else { + let formatter = strings.formatter().text(&error); + formatter.fmt(&strings.error.non_command_error) }; if let Err(error) = ctx.send(CreateReply::default().content(response).ephemeral(true)).await { diff --git a/discord/src/error.rs b/discord/src/error.rs index 258b374..df5fb7f 100644 --- a/discord/src/error.rs +++ b/discord/src/error.rs @@ -3,6 +3,8 @@ use std::fmt::Display; use poise::serenity_prelude as serenity; use squad_quest::error::MapError; +use crate::strings::ErrorStrings; + #[non_exhaustive] #[derive(Debug)] pub enum Error { @@ -23,6 +25,9 @@ pub enum Error { TimerSet, NotThisGuild, QuestLimitExceeded(u16), + BothUrlAndAttachment, + NoUrlOrAttachment, + NonImageAttachment, } impl From for Error { @@ -55,9 +60,9 @@ impl Display for Error { Self::QuestNotFound(u16) => write!(f, "quest #{u16} not found"), Self::QuestIsPublic(u16) => write!(f, "quest #{u16} is already public"), Self::QuestIsCompleted(u16) => write!(f, "quest #{u16} is already completed for this user"), - Self::NoContent => write!(f, "no text or attachment was specified"), - Self::NoChannelOrUser => write!(f, "no channel or user was specified"), - Self::BothChannelAndUser => write!(f, "both channel and user was specified"), + Self::NoContent => write!(f, "no text or attachment were specified"), + Self::NoChannelOrUser => write!(f, "no channel or user were specified"), + Self::BothChannelAndUser => write!(f, "both channel and user were specified"), Self::SerenityError(_) => write!(f, "discord interaction error"), Self::SquadQuestError(_) => write!(f, "internal logic error"), Self::AccountNotFound => write!(f, "account not found"), @@ -69,6 +74,9 @@ impl Display for Error { Self::TimerSet => write!(f, "timer is already set"), Self::NotThisGuild => write!(f, "cannot be used in this guild"), Self::QuestLimitExceeded(id) => write!(f, "exceeded limit for quest #{id}"), + Self::BothUrlAndAttachment => write!(f, "both url and attachment were specified"), + Self::NoUrlOrAttachment => write!(f, "no url or attachment were specified"), + Self::NonImageAttachment => write!(f, "attachment is not an image"), } } } @@ -90,11 +98,39 @@ impl std::error::Error for Error { Self::CannotReach(_) | Self::TimerSet | Self::NotThisGuild | - Self::QuestLimitExceeded(_) => None, + Self::QuestLimitExceeded(_) | + Self::BothUrlAndAttachment | + Self::NoUrlOrAttachment | + Self::NonImageAttachment => None, Self::SerenityError(error) => Some(error), Self::SquadQuestError(error) => Some(error), } } } - +impl<'a> Error { + pub fn formattable_string(&self, errors: &'a ErrorStrings) -> &'a str { + match self { + Self::QuestNotFound(_) => &errors.quest_not_found, + Self::QuestIsPublic(_) => &errors.quest_is_public, + Self::QuestIsCompleted(_) => &errors.quest_is_completed, + Self::NoContent => &errors.no_content, + Self::NoChannelOrUser => &errors.no_channel_or_user, + Self::BothChannelAndUser => &errors.both_channel_and_user, + Self::SerenityError(_) => &errors.discord_error, + Self::SquadQuestError(_) => &errors.library_error, + Self::AccountNotFound => &errors.account_not_found, + Self::AccountIsSelf => &errors.account_is_self, + Self::InsufficientFunds(_) => &errors.insufficient_funds, + Self::RoomNotFound(_) => &errors.room_not_found, + Self::RoomAlreadyUnlocked(_) => &errors.room_already_unlocked, + Self::CannotReach(_) => &errors.cannot_reach, + Self::TimerSet => &errors.timer_set, + Self::NotThisGuild => &errors.not_this_guild, + Self::QuestLimitExceeded(_) => &errors.quest_limit_exceeded, + Self::BothUrlAndAttachment => &errors.both_url_and_attachment, + Self::NoUrlOrAttachment => &errors.no_url_or_attachment, + Self::NonImageAttachment => &errors.non_image_attachment, + } + } +} diff --git a/discord/src/main.rs b/discord/src/main.rs index ffe6922..acb3fea 100644 --- a/discord/src/main.rs +++ b/discord/src/main.rs @@ -92,6 +92,7 @@ async fn main() { commands::account::reset(), commands::map::unlock(), commands::map::r#move(), + commands::map::avatar(), ], ..Default::default() }) diff --git a/discord/src/strings.rs b/discord/src/strings.rs index 43505d4..c965f8c 100644 --- a/discord/src/strings.rs +++ b/discord/src/strings.rs @@ -142,6 +142,7 @@ pub struct Strings { pub scoreboard: Scoreboard, pub social: Social, pub quest: QuestStrings, + pub error: ErrorStrings, } impl Default for Strings { @@ -160,6 +161,7 @@ impl Default for Strings { social: Social::default(), account: AccountReplies::default(), map: MapReplies::default(), + error: ErrorStrings::default(), } } } @@ -391,6 +393,8 @@ impl Default for AccountReplies { pub struct MapReplies { pub room_unlocked: String, pub moved_to_room: String, + pub updated_avatar: String, + pub processing_url: String, } impl Default for MapReplies { @@ -398,6 +402,62 @@ impl Default for MapReplies { Self { room_unlocked: "Unlocked room #{value}. Your balance: {b.current}".to_string(), moved_to_room: "Moved to room #{value}".to_string(), + updated_avatar: "Successfully changed avatar".to_string(), + processing_url: "Processing URL...".to_string(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(default)] +pub struct ErrorStrings { + pub non_command_error: String, + pub quest_not_found: String, + pub quest_is_public: String, + pub quest_is_completed: String, + pub no_content: String, + pub no_channel_or_user: String, + pub both_channel_and_user: String, + pub discord_error: String, + pub library_error: String, + pub account_not_found: String, + pub account_is_self: String, + pub insufficient_funds: String, + pub room_not_found: String, + pub room_already_unlocked: String, + pub cannot_reach: String, + pub timer_set: String, + pub not_this_guild: String, + pub quest_limit_exceeded: String, + pub both_url_and_attachment: String, + pub no_url_or_attachment: String, + pub non_image_attachment: String, +} + +impl Default for ErrorStrings { + fn default() -> Self { + Self { + non_command_error: "Internal server error: {text}".to_string(), + quest_not_found: "Quest {q.id} not found".to_string(), + quest_is_public: "Quest {q.id} is already public".to_string(), + quest_is_completed: "Quest {q.id} is already completed for this user".to_string(), + no_content: "No text or attachment were specified".to_string(), + no_channel_or_user: "No channel or user were specified".to_string(), + both_channel_and_user: "Both channel and user were specified".to_string(), + discord_error: "Discord interaction error: {text}".to_string(), + library_error: "Some internal logic error: {text}".to_string(), + account_not_found: "Given account was not found".to_string(), + account_is_self: "Given account is the same as command invoker".to_string(), + insufficient_funds: "You don't have {value} points".to_string(), + room_not_found: "Room #{value} not found".to_string(), + room_already_unlocked: "Room #{value} is already unlocked for this account".to_string(), + cannot_reach: "You cannot reach room #{value}".to_string(), + timer_set: "Timer is already set".to_string(), + not_this_guild: "Bot cannot be used in this guild".to_string(), + quest_limit_exceeded: "Exceeded limit for quest {q.id}".to_string(), + both_url_and_attachment: "Both URL and attachment were specified".to_string(), + no_url_or_attachment: "No URL or attachment were specified".to_string(), + non_image_attachment: "Given attachment is not an image".to_string(), } } }