From 0cb536b42bb5447094dd205c9d2bc90b621eecaa Mon Sep 17 00:00:00 2001 From: yourfriendoss Date: Tue, 9 Sep 2025 10:15:15 +0300 Subject: [PATCH] first commit --- .cargo/config.toml | 2 + .gitignore | 4 + .gitmodules | 3 + ...1010dc1581eb4cbff6a55d1fbfd770c42c23c.json | 16 + Cargo.lock | 4328 +++++++++++++++++ Cargo.toml | 38 + README.md | 37 + migrations/20250826132657_init.down.sql | 1 + migrations/20250826132657_init.up.sql | 5 + migrations/20250827155307_cooldown.down.sql | 1 + migrations/20250827155307_cooldown.up.sql | 5 + rust-toolchain.toml | 2 + src/client.rs | 560 +++ src/commands/argument.rs | 188 + src/commands/eco/balance.rs | 72 + src/commands/eco/cf.rs | 143 + src/commands/eco/farm.rs | 704 +++ src/commands/eco/fish.rs | 416 ++ src/commands/eco/inv.rs | 153 + src/commands/eco/mod.rs | 15 + src/commands/eco/shop.rs | 209 + src/commands/midi/mod.rs | 190 + src/commands/midi/play.rs | 263 + src/commands/midi/playlist.rs | 362 ++ src/commands/midi/queue.rs | 62 + src/commands/midi/skip.rs | 66 + src/commands/midi/stop.rs | 57 + src/commands/mod.rs | 87 + src/commands/system/about.rs | 60 + src/commands/system/follow.rs | 162 + src/commands/system/help.rs | 111 + src/commands/system/launch.rs | 53 + src/commands/system/mod.rs | 3 + src/commands/system/test.rs | 45 + src/commands/system/translate.rs | 158 + src/log.rs | 114 + src/main.rs | 218 + src/midi_helper.rs | 131 + 38 files changed, 9044 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .sqlx/query-74ca8a62c49f583650b8bc524c61010dc1581eb4cbff6a55d1fbfd770c42c23c.json create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 migrations/20250826132657_init.down.sql create mode 100644 migrations/20250826132657_init.up.sql create mode 100644 migrations/20250827155307_cooldown.down.sql create mode 100644 migrations/20250827155307_cooldown.up.sql create mode 100644 rust-toolchain.toml create mode 100644 src/client.rs create mode 100644 src/commands/argument.rs create mode 100644 src/commands/eco/balance.rs create mode 100644 src/commands/eco/cf.rs create mode 100644 src/commands/eco/farm.rs create mode 100644 src/commands/eco/fish.rs create mode 100644 src/commands/eco/inv.rs create mode 100644 src/commands/eco/mod.rs create mode 100644 src/commands/eco/shop.rs create mode 100644 src/commands/midi/mod.rs create mode 100644 src/commands/midi/play.rs create mode 100644 src/commands/midi/playlist.rs create mode 100644 src/commands/midi/queue.rs create mode 100644 src/commands/midi/skip.rs create mode 100644 src/commands/midi/stop.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/system/about.rs create mode 100644 src/commands/system/follow.rs create mode 100644 src/commands/system/help.rs create mode 100644 src/commands/system/launch.rs create mode 100644 src/commands/system/mod.rs create mode 100644 src/commands/system/test.rs create mode 100644 src/commands/system/translate.rs create mode 100644 src/log.rs create mode 100644 src/main.rs create mode 100644 src/midi_helper.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..bff29e6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00514d5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +midis/*.mid +.env +config.hocon diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..b725797 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "midiplayer_rs"] + path = midiplayer_rs + url = https://github.com/ar065/midiplayer_rs diff --git a/.sqlx/query-74ca8a62c49f583650b8bc524c61010dc1581eb4cbff6a55d1fbfd770c42c23c.json b/.sqlx/query-74ca8a62c49f583650b8bc524c61010dc1581eb4cbff6a55d1fbfd770c42c23c.json new file mode 100644 index 0000000..5adbd3e --- /dev/null +++ b/.sqlx/query-74ca8a62c49f583650b8bc524c61010dc1581eb4cbff6a55d1fbfd770c42c23c.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO users (_id, balance, items) VALUES ($1, $2, $3)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Int4", + "Json" + ] + }, + "nullable": [] + }, + "hash": "74ca8a62c49f583650b8bc524c61010dc1581eb4cbff6a55d1fbfd770c42c23c" +} diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..1b21156 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4328 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.60.2", +] + +[[package]] +name = "anyhow" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" + +[[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.106", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[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 = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bitflags" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +dependencies = [ + "serde", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cap" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f125eb85b84a24c36b02ed1d22c9dd8632f53b3cde6e4d23512f94021030003" + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clap" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc0e74a703892159f5ae7d3aac52c8e6c392f5ae5f359c70b5881d60aaac318" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e7f4214277f3c7aa526a59dd3fbe306a370daee1f8b7b8c987069cd8e888a8" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.5.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9501bd3f5f09f7bbee01da9a511073ed30a80cd7a509f1214bb74eadea71ad" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_derive" +version = "4.5.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", + "url", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "compact_str" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86b9c4c00838774a6d902ef931eff7470720c51d90c2e32cfe15dc304737b3f" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console-api" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857" +dependencies = [ + "futures-core", + "prost", + "prost-types", + "tonic", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6539aa9c6a4cd31f4b1c040f860a1eac9aa80e7df6b05d506a6e7179936d6a01" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures-task", + "hdrhistogram", + "humantime", + "hyper-util", + "prost", + "prost-types", + "serde", + "serde_json", + "thread_local", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" +dependencies = [ + "bitflags", + "crossterm_winapi", + "futures-core", + "libc", + "mio 0.8.11", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenv_codegen" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56966279c10e4f8ee8c22123a15ed74e7c8150b658b26c619c53f4a56eb4a8aa" +dependencies = [ + "dotenv_codegen_implementation", + "proc-macro-hack", +] + +[[package]] +name = "dotenv_codegen_implementation" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e737a3522cd45f6adc19b644ce43ef53e1e9045f2d2de425c1f468abd4cf33" +dependencies = [ + "dotenv", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2", + "subtle", + "zeroize", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" +dependencies = [ + "encoding-index-japanese", + "encoding-index-korean", + "encoding-index-simpchinese", + "encoding-index-singlebyte", + "encoding-index-tradchinese", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +dependencies = [ + "encoding_index_tests", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check 0.9.5", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.11.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c9198f173dd59ce26ff9f95ef0aafd0a0fe01fb9d72841bc5066a4c06511d" +dependencies = [ + "base64 0.21.7", + "byteorder", + "flate2", + "nom 7.1.3", + "num-traits", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hocon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dbf0338dac82c09762a1283d8bb117f2d0dddb97ee4eec711dd1b78f89975ee" +dependencies = [ + "aho-corasick 0.7.20", + "java-properties", + "lazy_static", + "linked-hash-map", + "memchr", + "nom 4.2.3", + "serde", + "serde_path_to_error", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[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", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" + +[[package]] +name = "hyper" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "io-uring" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "java-properties" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1904d8654a1ef51034d02d5a9411b50bf91bea15b0ab644ae179d1325976263" +dependencies = [ + "encoding", + "lazy_static", + "regex", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + +[[package]] +name = "libredox" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" +dependencies = [ + "bitflags", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "midiplayer_rs" +version = "0.1.0" +dependencies = [ + "clap", + "lazy_static", + "libloading", + "thousands", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mpp-rs" +version = "0.1.0" +dependencies = [ + "async-trait", + "cap", + "chrono", + "console-subscriber", + "dotenv_codegen", + "flume", + "futures-util", + "hocon", + "midiplayer_rs", + "rand 0.9.2", + "rayon", + "reqwest", + "rustls", + "rustls-rustcrypto", + "serde", + "serde_json", + "sqlx", + "thousands", + "tokio", + "tokio-console", + "tokio-tungstenite", + "tracing-subscriber", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "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]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "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]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +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.3", +] + +[[package]] +name = "ratatui" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44c9e68fd46eda15c646fbb85e1040b657a58cdc8c98db1d97a55930d991eef" +dependencies = [ + "bitflags", + "cassowary", + "compact_str", + "crossterm", + "itertools 0.12.1", + "lru", + "paste", + "stability", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick 1.1.3", + "memchr", + "regex-automata 0.4.10", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick 1.1.3", + "memchr", + "regex-syntax 0.8.6", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + +[[package]] +name = "reqwest" +version = "0.12.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower 0.5.2", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.2", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.4", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-rustcrypto" +version = "0.0.2-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557" +dependencies = [ + "aead", + "aes-gcm", + "chacha20poly1305", + "crypto-common", + "der", + "digest", + "ecdsa", + "ed25519-dalek", + "hmac", + "p256", + "p384", + "paste", + "pkcs8", + "rand_core 0.6.4", + "rsa", + "rustls", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "sec1", + "sha2", + "signature", + "x25519-dalek", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "serde_json" +version = "1.0.143" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio 0.8.11", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.11.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.16", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.106", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.106", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.16", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.16", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.16", + "tracing", + "url", +] + +[[package]] +name = "stability" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d904e7009df136af5297832a3ace3370cd14ff1546a232f4f185036c2736fcac" +dependencies = [ + "quote", + "syn 2.0.106", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.106", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl 2.0.16", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + +[[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 = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "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.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio 1.0.4", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-console" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c6bf2c27092e9a31a99f361f7bbaae50572039a5531d4c73d9b80bf7b90316" +dependencies = [ + "clap", + "clap_complete", + "color-eyre", + "console-api", + "crossterm", + "dirs", + "futures", + "h2", + "hdrhistogram", + "humantime", + "hyper-util", + "once_cell", + "prost-types", + "ratatui", + "regex", + "serde", + "tokio", + "toml", + "tonic", + "tower 0.4.13", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "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.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2 0.5.10", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.2", + "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" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +dependencies = [ + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.9.2", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 2.0.16", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] + +[[package]] +name = "webpki-roots" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..284f6bb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "mpp-rs" +version = "0.1.0" +edition = "2024" + + +[dependencies] +rustls = { version = "0.23.31", default-features = false, features = [ + "logging", + "tls12", +] } +midiplayer_rs = { path = "./midiplayer_rs" } +rustls-rustcrypto = "0.0.2-alpha" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = { version = "1.0.143", features=["raw_value"]} +futures-util = "0.3.31" +rand = "0.9.2" +tokio = { version = "1.47.1", features = ["full", "tracing"] } +tokio-tungstenite = { version = "*", features = ["rustls-tls-webpki-roots"] } +chrono = "0.4.41" +flume = "0.11.1" +async-trait = "0.1.89" +reqwest = { version = "0.12.23", default-features = false, features = [ + "rustls-tls-webpki-roots-no-provider", + "http2", + "charset", + "blocking", +] } +tokio-console = "0.1" +tracing-subscriber = "0.3.19" +console-subscriber = "0.4.1" +sqlx = { version = "0.8", features = [ "runtime-tokio", "postgres", "chrono" ] } +dotenv_codegen = "0.15.0" +cap = "0.1.2" +thousands = "0.2.0" +rayon = "1.11.0" + +hocon = {version = "0.9.0", default-features = false, features = ["serde-support"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..df94184 --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# copper + +Copper is a smart MultiPlayerPiano bot written in Rust. + +It features Austin's (nitsua) midi parser and player, which is meant for high-performance and to be able to parse black midis. + +## Warning +Clone this repository with `git clone --recursive https://git.sad.ovh/sophie/copper`. + +## Features +- 2 billion note per second parser with all modern midi features supported +- Full economy system written using `sqlx` and postgres +- Lots of generic MPP bot features such as following, moving between rooms, et cetera + +## Todo +- Playlist has hardcoded Playlist +- Get rid of all mentions of files.sad.ovh as it's my copyparty server, and make it customizable to other servers. + +## Running +1. Requirements: Rust Nightly, should be automatically used when you do cargo run. +2. `cargo install` +3. Add config.hocon, example: +```hocon +database { + url: "postgres://user:pass@ip:port/database" +} +commands { + prefix: "r" + name: "Copper" +} +client { + token: "token" + ws: "wss://mppclone.com" + room: "cheez" +} +``` +4. Simple as that, `cargo run`. diff --git a/migrations/20250826132657_init.down.sql b/migrations/20250826132657_init.down.sql new file mode 100644 index 0000000..cc1f647 --- /dev/null +++ b/migrations/20250826132657_init.down.sql @@ -0,0 +1 @@ +DROP TABLE users; diff --git a/migrations/20250826132657_init.up.sql b/migrations/20250826132657_init.up.sql new file mode 100644 index 0000000..c83e205 --- /dev/null +++ b/migrations/20250826132657_init.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE users ( + _id TEXT PRIMARY KEY, + balance INTEGER NOT NULL DEFAULT 0, + items JSON NOT NULL DEFAULT '[]' +); diff --git a/migrations/20250827155307_cooldown.down.sql b/migrations/20250827155307_cooldown.down.sql new file mode 100644 index 0000000..48c66a2 --- /dev/null +++ b/migrations/20250827155307_cooldown.down.sql @@ -0,0 +1 @@ +DROP TABLE cooldowns; diff --git a/migrations/20250827155307_cooldown.up.sql b/migrations/20250827155307_cooldown.up.sql new file mode 100644 index 0000000..0d54055 --- /dev/null +++ b/migrations/20250827155307_cooldown.up.sql @@ -0,0 +1,5 @@ +CREATE TABLE cooldowns ( + _id TEXT, + type TEXT NOT NULL PRIMARY KEY, + untill TIMESTAMPTZ NOT NULL +); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5d56faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..2f4c707 --- /dev/null +++ b/src/client.rs @@ -0,0 +1,560 @@ +use futures_util::{SinkExt, StreamExt}; +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{Mutex, RwLock}; +use tokio::time::{Duration, interval, sleep}; +use tokio_tungstenite::{connect_async, tungstenite::Message}; + +use crate::log; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Player { + pub id: String, + #[serde(rename = "_id")] + pub _id: String, + pub name: String, + pub color: String, + pub tag: Option, + pub crown: Option, + #[serde(default, deserialize_with = "deserialize_f64_from_str_or_num")] + pub x: f64, + #[serde(default, deserialize_with = "deserialize_f64_from_str_or_num")] + pub y: f64, +} + +fn deserialize_f64_from_str_or_num<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + use serde::de::Error; + use serde_json::Value; + + let value = Value::deserialize(deserializer)?; + match value { + Value::Number(num) => num + .as_f64() + .ok_or_else(|| D::Error::custom("Invalid number for f64")), + Value::String(s) => s + .parse::() + .map_err(|_| D::Error::custom(format!("Invalid string for f64: {}", s))), + Value::Null => Ok(0.0), + _ => Err(D::Error::custom("Expected string or number for f64")), + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Tag { + pub text: String, + pub color: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Crown { + #[serde(rename = "endPos")] + pub end_pos: Position, + #[serde(rename = "startPos")] + pub start_pos: Position, + #[serde(rename = "userId")] + pub user_id: Option, + pub time: u64, + #[serde(rename = "participantId")] + pub participant_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Position { + #[serde(deserialize_with = "deserialize_f64_from_str_or_num")] + pub x: f64, + #[serde(deserialize_with = "deserialize_f64_from_str_or_num")] + pub y: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ChannelSettings { + pub chat: Option, + pub color: Option, + pub color2: Option, + pub visible: Option, + pub limit: Option, + pub crownsolo: Option, + #[serde(rename = "no cussing")] + pub no_cussing: Option, + #[serde(rename = "minOnlineTime")] + pub min_online_time: Option, + #[serde(rename = "allowBots")] + pub allow_bots: Option, + #[serde(rename = "noindex")] + pub no_index: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Channel { + pub _id: String, + pub count: u32, + pub crown: Option, + pub id: String, + pub settings: ChannelSettings, +} + +#[derive(Debug, Clone)] +pub enum ClientEvent { + Connected, + Message { player: Player, message: String }, + PlayerJoined(Player), + PlayerLeft(String), + Mouse { x: f64, y: f64, id: String }, + NameChanged { now: Player, before: Player }, + ChannelInfo(Channel), + Sync { e: i64 }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Note { + pub n: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub v: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub d: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub s: Option, +} +#[derive(Default, Debug)] +struct NoteBuffer { + start: Option, + notes: Vec, +} + +#[derive(Debug, Clone)] +pub struct Client { + ws_url: String, + pub channel: String, + token: String, + me: Arc>>, + players: Arc>>, + settings: Arc>>, + event_tx: flume::Sender, + message_tx: flume::Sender, + command_tx: flume::Sender, + message_rx: flume::Receiver, + command_rx: flume::Receiver, + + note_buffer: Arc>, + server_time_offset: Arc>, + connected_tx: tokio::sync::watch::Sender, +} + +impl Client { + pub fn new() -> (Self, flume::Receiver) { + let (event_tx, event_rx) = flume::bounded(100); + let (message_tx, message_rx) = flume::unbounded(); + let (command_tx, command_rx) = flume::unbounded(); + + let (connected_tx, _connected_rx) = tokio::sync::watch::channel(false); + + let client = Client { + ws_url: String::new(), + channel: String::new(), + token: String::new(), + me: Arc::new(RwLock::new(None)), + players: Arc::new(RwLock::new(HashMap::new())), + settings: Arc::new(RwLock::new(None)), + event_tx, + message_tx, + command_tx, + message_rx, + command_rx, + note_buffer: Arc::new(Mutex::new(NoteBuffer::default())), + server_time_offset: Arc::new(Mutex::new(0)), + + connected_tx, + }; + + client.clone().start_note_flush_task(); + + (client, event_rx) + } + + fn start_note_flush_task(self) { + let note_buffer = self.note_buffer.clone(); + let server_time_offset = self.server_time_offset.clone(); + let client = self.clone(); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis(200)); + + loop { + interval.tick().await; + + let maybe_batch = { + let mut buf = note_buffer.lock().await; + if buf.notes.is_empty() || buf.start.is_none() { + None + } else { + let offset = *server_time_offset.lock().await; + let time = buf.start.unwrap() + offset; + buf.start = None; + Some((std::mem::take(&mut buf.notes), time)) + } + }; + + if let Some((notes_to_send, time)) = maybe_batch { + client.note(notes_to_send, time).await; + } + } + }); + } + + pub async fn connect( + &mut self, + ws_url: String, + token: String, + channel: String, + ) -> Result<(), Box> { + self.ws_url = ws_url.clone(); + self.token = token.clone(); + self.channel = channel.clone(); + + self.run().await + } + + pub async fn wait_connected(&self) { + let mut rx = self.connected_tx.subscribe(); + loop { + if *rx.borrow() { + return; + } + if rx.changed().await.is_err() { + return; + } + } + } + + async fn run(&self) -> Result<(), Box> { + loop { + match self.connect_websocket().await { + Ok(_) => { + log!(DISCONNECTED); + } + Err(e) => { + log!(ERRORED, e); + } + } + sleep(Duration::from_secs(1)).await; + } + } + + async fn connect_websocket(&self) -> Result<(), Box> { + let (ws_stream, _) = connect_async(&self.ws_url).await?; + let (mut ws_sender, mut ws_receiver) = ws_stream.split(); + + let hi_msg = json!([{"m": "hi", "token": self.token}]); + ws_sender + .send(Message::Text(hi_msg.to_string().into())) + .await?; + + let heartbeat_tx = self.command_tx.clone(); + tokio::spawn(async move { + let mut interval = interval(Duration::from_secs(1)); + loop { + interval.tick().await; + let _ = heartbeat_tx + .send(json!({"m": "t", "e": chrono::Utc::now().timestamp_millis()})); + } + }); + + let msg_command_tx = self.command_tx.clone(); + let message_rx = self.message_rx.clone(); + + tokio::spawn(async move { + let mut interval = interval(Duration::from_millis(700)); + loop { + interval.tick().await; + if let Ok(message) = message_rx.try_recv() { + let _ = msg_command_tx.send(json!({"m": "a", "message": message})); + } + } + }); + + let out_command_rx = self.command_rx.clone(); + tokio::spawn(async move { + while let Ok(command) = out_command_rx.recv_async().await { + let msg = json!([command]); + if ws_sender + .send(Message::Text(msg.to_string().into())) + .await + .is_err() + { + break; + } + } + }); + + let command_sender = self.command_tx.clone(); + + while let Some(msg) = ws_receiver.next().await { + match msg? { + Message::Text(text) => { + if let Ok(json_array) = serde_json::from_str::>(&text) { + for message in json_array { + self.handle_message(message, &command_sender).await; + } + } + } + Message::Close(_) => break, + _ => {} + } + } + + let _ = self.connected_tx.send(false); + + Ok(()) + } + + async fn handle_message(&self, message: Value, command_tx: &flume::Sender) { + if let Some(m) = message.get("m").and_then(|v| v.as_str()) { + match m { + "hi" => { + if let Ok(player) = serde_json::from_value::(message["u"].clone()) { + *self.me.write().await = Some(player); + } + let _ = command_tx.send(json!({"m": "ch", "_id": self.channel})); + } + "t" => { + if let Some(server_time) = message.get("t").and_then(|v| v.as_i64()) { + let echo = message.get("e").and_then(|v| v.as_i64()); + self.update_server_time(server_time, echo).await; + let _ = self + .event_tx + .send_async(ClientEvent::Sync { + e: echo.unwrap_or(0), + }) + .await; + } + } + "ch" => { + if let Ok(channel) = serde_json::from_value::(message["ch"].clone()) { + if let Some(me) = self.me.write().await.as_mut() { + me.crown = Some( + channel + .crown + .as_ref() + .and_then(|c| c.user_id.as_ref()) + .map(|id| id == &me._id) + .unwrap_or(false), + ); + } + + *self.settings.write().await = Some(channel.settings.clone()); + + if let Some(ppl) = message["ppl"].as_array() { + let mut players = self.players.write().await; + players.clear(); + + for player_val in ppl { + let player = + serde_json::from_value::(player_val.clone()).unwrap(); + + players.insert(player.id.clone(), player.clone()); + let _ = self.event_tx.send(ClientEvent::PlayerJoined(player)); + } + + log!(JOINED, self.channel, players.len(), channel.count); + } + + let _ = self.event_tx.send(ClientEvent::ChannelInfo(channel)); + let _ = self.event_tx.send(ClientEvent::Connected); + + let _ = self.connected_tx.send(true); + } + } + "p" => { + if let Ok(player) = serde_json::from_value::(message.clone()) { + let mut players = self.players.write().await; + + if let Some(existing) = players.get(&player.id) { + if existing.name != player.name { + let _ = self.event_tx.send(ClientEvent::NameChanged { + now: player.clone(), + before: existing.clone(), + }); + } + players.insert(player.id.clone(), player); + } else { + players.insert(player.id.clone(), player.clone()); + let _ = self.event_tx.send(ClientEvent::PlayerJoined(player)); + } + } + } + "bye" => { + if let Some(player_id) = message.get("p").and_then(|v| v.as_str()) { + self.players.write().await.remove(player_id); + let _ = self + .event_tx + .send(ClientEvent::PlayerLeft(player_id.to_string())); + } + } + "a" => { + if let (Ok(player), Some(msg)) = ( + serde_json::from_value::(message["p"].clone()), + message.get("a").and_then(|v| v.as_str()), + ) { + let _ = self.event_tx.send(ClientEvent::Message { + player, + message: msg.to_string(), + }); + } + } + "m" => { + if let (Some(x), Some(y), Some(id)) = ( + message + .get("x") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + message + .get("y") + .and_then(|v| v.as_str()) + .and_then(|s| s.parse().ok()), + message.get("id").and_then(|v| v.as_str()), + ) { + let _ = self.event_tx.send(ClientEvent::Mouse { + x, + y, + id: id.to_string(), + }); + } + } + _ => {} + } + } + } + async fn update_server_time(&self, server_time: i64, _: Option) { + let now = chrono::Utc::now().timestamp_millis(); + let target = server_time - now; + + let offset_lock = self.server_time_offset.lock().await; + let current = *offset_lock; + let diff = target - current; + + let steps = 50; + let inc = diff / steps; + let offset_arc = self.server_time_offset.clone(); + + tokio::spawn(async move { + for _ in 0..steps { + { + let mut off = offset_arc.lock().await; + *off += inc; + } + tokio::time::sleep(Duration::from_millis(20)).await; + } + { + let mut off = offset_arc.lock().await; + *off = target; + } + }); + } + pub async fn send_command(&self, command: Value) { + let _ = self.command_tx.send(command); + } + + pub async fn userset(&self, name: Option<&str>, color: Option<&str>) { + let mut set = json!({}); + if let Some(n) = name { + set["name"] = json!(n); + } + if let Some(c) = color { + set["color"] = json!(c); + } + + self.send_command(json!({"m": "userset", "set": set})).await; + } + + pub async fn channelset(&self, settings: ChannelSettings) { + self.send_command(json!({"m": "chset", "set": settings})) + .await; + } + + pub async fn message>(&self, message: S) { + let message = message.as_ref(); + let chunks: Vec<&str> = message + .char_indices() + .step_by(450) + .map(|(i, _)| { + let end = (i + 450).min(message.len()); + &message[i..end] + }) + .collect(); + + for chunk in chunks { + let _ = self.message_tx.send(chunk.to_string()); + } + } + + pub async fn move_to(&self, x: f64, y: f64) { + self.send_command(json!({ + "m": "m", + "x": format!("{:.2}", x), + "y": format!("{:.2}", y) + })) + .await; + } + + pub async fn find_user(&self, id_or_name: &str) -> Option { + let players = self.players.read().await; + players + .values() + .find(|p| { + p.id == id_or_name || p.name.to_lowercase().contains(&id_or_name.to_lowercase()) + }) + .cloned() + } + + pub async fn give_crown(&self, id: &str) { + self.send_command(json!({"m": "chown", "id": id})).await; + } + + pub async fn kickban(&self, id: &str, time: u64) { + self.send_command(json!({"m": "kickban", "_id": id, "ms": time})) + .await; + } + + pub async fn unban(&self, id: &str) { + self.send_command(json!({"m": "unban", "_id": id})).await; + } + + pub async fn get_me(&self) -> Option { + self.me.read().await.clone() + } + + pub async fn get_players(&self) -> Vec { + self.players.read().await.values().cloned().collect() + } + + async fn note(&self, notes: Vec, time: i64) { + let payload = json!({ + "m": "n", + "t": time, + "n": notes + }); + + let _ = self.send_command(payload).await; + } + + pub async fn add_note_buffer(&self, note: Note) { + let now = chrono::Utc::now().timestamp_millis(); + let mut buf = self.note_buffer.lock().await; + + let mut value = serde_json::to_value(¬e).unwrap(); + + if let Some(start) = buf.start { + value["d"] = json!(now - start); + } else { + buf.start = Some(now); + value.as_object_mut().unwrap().remove("d"); + } + + buf.notes.push(value); + } +} diff --git a/src/commands/argument.rs b/src/commands/argument.rs new file mode 100644 index 0000000..84379d8 --- /dev/null +++ b/src/commands/argument.rs @@ -0,0 +1,188 @@ +use std::{collections::HashMap, fmt}; + +#[derive(Debug, Clone)] +pub enum ArgumentType { + String, + Integer, + Float, + Boolean, + Enum(&'static [&'static str]), + GreedyString, +} + +impl fmt::Display for ArgumentType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ArgumentType::String => write!(f, "String"), + ArgumentType::Integer => write!(f, "Integer"), + ArgumentType::Enum(variants) => write!(f, "Enum({:?})", variants), + ArgumentType::Float => write!(f, "Float"), + ArgumentType::Boolean => write!(f, "Boolean"), + ArgumentType::GreedyString => write!(f, "GreedyString"), + } + } +} + +#[derive(Debug, Clone)] +pub struct ArgumentSpec { + pub name: &'static str, + pub arg_type: ArgumentType, + pub required: bool, + pub default: Option<&'static str>, +} + +#[derive(Debug, Clone)] +pub enum ParsedArgument { + String(String), + Integer(i64), + Float(f64), + Boolean(bool), + Enum(String), + GreedyString(String), +} + +impl fmt::Display for ParsedArgument { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParsedArgument::String(s) => write!(f, "{}", s), + ParsedArgument::Integer(i) => write!(f, "{}", i), + ParsedArgument::Float(fl) => write!(f, "{}", fl), + ParsedArgument::Boolean(b) => write!(f, "{}", b), + ParsedArgument::Enum(e) => write!(f, "{}", e), + ParsedArgument::GreedyString(s) => write!(f, "{}", s), + } + } +} + +pub fn parse_argument(arg: &str, spec: &ArgumentSpec) -> Result { + match &spec.arg_type { + ArgumentType::String => Ok(ParsedArgument::String(arg.to_string())), + ArgumentType::Integer => arg + .parse::() + .map(ParsedArgument::Integer) + .map_err(|_| format!("Expected integer for '{}', got '{}'", spec.name, arg)), + ArgumentType::Float => arg + .parse::() + .map(ParsedArgument::Float) + .map_err(|_| format!("Expected float for '{}', got '{}'", spec.name, arg)), + ArgumentType::Boolean => match arg.to_lowercase().as_str() { + "true" | "yes" | "1" => Ok(ParsedArgument::Boolean(true)), + "false" | "no" | "0" => Ok(ParsedArgument::Boolean(false)), + _ => Err(format!( + "Expected boolean for '{}', got '{}'", + spec.name, arg + )), + }, + ArgumentType::Enum(variants) => { + if variants.contains(&arg) { + Ok(ParsedArgument::Enum(arg.to_string())) + } else { + Err(format!( + "Argument '{}' must be one of {:?}, got '{}'", + spec.name, variants, arg + )) + } + } + ArgumentType::GreedyString => Ok(ParsedArgument::GreedyString(arg.to_string())), + } +} + +#[derive(Debug, Clone)] +pub struct ParsedArguments { + pub raw: Vec, + pub parsed: HashMap<&'static str, ParsedArgument>, +} + +impl ParsedArguments { + pub fn get(&self, name: &str) -> Option<&ParsedArgument> { + self.parsed.get(name) + } +} + +pub fn parse_arguments( + specs: &[ArgumentSpec], + raw_args: &[String], +) -> Result { + let mut parsed = HashMap::new(); + let raw_vec = raw_args.to_vec(); + + let mut i = 0; + while i < specs.len() { + let spec = &specs[i]; + + let raw_value = match &spec.arg_type { + ArgumentType::GreedyString => { + if i >= raw_args.len() { + spec.default.map(|s| s.to_string()) + } else { + Some(raw_args[i..].join(" ")) + } + } + _ => raw_args + .get(i) + .cloned() + .or_else(|| spec.default.map(|s| s.to_string())), + }; + + match raw_value { + Some(ref value) => { + let parsed_arg = match &spec.arg_type { + ArgumentType::String => ParsedArgument::String(value.clone()), + ArgumentType::Integer => value + .parse::() + .map(ParsedArgument::Integer) + .map_err(|_| { + format!( + "Argument '{}' must be an integer, got '{}'", + spec.name, value + ) + })?, + ArgumentType::Float => value + .parse::() + .map(ParsedArgument::Float) + .map_err(|_| { + format!("Argument '{}' must be a float, got '{}'", spec.name, value) + })?, + ArgumentType::Boolean => match value.to_lowercase().as_str() { + "true" | "yes" | "1" => ParsedArgument::Boolean(true), + "false" | "no" | "0" => ParsedArgument::Boolean(false), + _ => { + return Err(format!( + "Argument '{}' must be a boolean, got '{}'", + spec.name, value + )); + } + }, + ArgumentType::Enum(variants) => { + if variants.contains(&value.as_str()) { + ParsedArgument::Enum(value.clone()) + } else { + return Err(format!( + "Argument '{}' must be one of {:?}, got '{}'", + spec.name, variants, value + )); + } + } + ArgumentType::GreedyString => ParsedArgument::GreedyString(value.clone()), + }; + + parsed.insert(spec.name, parsed_arg); + + if let ArgumentType::GreedyString = spec.arg_type { + break; + } + } + None if spec.required => { + return Err(format!("Missing required argument '{}'", spec.name)); + } + None => {} + } + + i += 1; + } + + Ok(ParsedArguments { + raw: raw_vec, + parsed, + }) +} diff --git a/src/commands/eco/balance.rs b/src/commands/eco/balance.rs new file mode 100644 index 0000000..37752df --- /dev/null +++ b/src/commands/eco/balance.rs @@ -0,0 +1,72 @@ +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use std::sync::Arc; + +use async_trait::async_trait; +use sqlx::Pool; +use sqlx::Postgres; + +pub struct BalanceCommand { + pool: Arc>, +} + +impl BalanceCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } +} +#[async_trait] +impl Command for BalanceCommand { + fn name(&self) -> &'static str { + "balance" + } + fn aliases(&self) -> &[&'static str] { + &["bal"] + } + + fn category(&self) -> &'static str { + "eco" + } + fn description(&self) -> &'static str { + "View your balance." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "user_id", + arg_type: ArgumentType::String, + required: false, + default: None, + }] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let id = match args.get("user_id") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => player._id.as_str(), + }; + + let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap(); + + match user { + Some(user) => { + client + .message(format!("You have {} coins.", user.balance)) + .await; + } + None => { + client.message("No user found.").await; + } + } + } +} diff --git a/src/commands/eco/cf.rs b/src/commands/eco/cf.rs new file mode 100644 index 0000000..0ebd12b --- /dev/null +++ b/src/commands/eco/cf.rs @@ -0,0 +1,143 @@ +use crate::User; +use crate::client::{Client, ClientEvent, Player}; +use crate::commands::Command; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use async_trait::async_trait; +use rand::Rng; +use sqlx::Pool; +use sqlx::Postgres; +use std::sync::Arc; +use tokio::time::{Duration, sleep}; + +pub struct CoinflipCommand { + pool: Arc>, +} + +impl CoinflipCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } +} + +#[async_trait] +impl Command for CoinflipCommand { + fn name(&self) -> &'static str { + "coinflip" + } + + fn aliases(&self) -> &[&'static str] { + &["cf"] + } + + fn category(&self) -> &'static str { + "eco" + } + + fn description(&self) -> &'static str { + "Flip a coin 50/50, optionally bet some coins." + } + + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "bet_amount", + arg_type: ArgumentType::Integer, + required: false, + default: None, + }] + } + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let bet_amount: Option = match args.get("bet_amount") { + Some(ParsedArgument::Integer(i)) => Some(*i as i32), + _ => None, + }; + + let user_opt = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap(); + + let mut user = match user_opt { + Some(u) => u, + None => { + client.message("No user found.").await; + return; + } + }; + + let choice = if rand::rng().random_bool(0.5) { + "Heads" + } else { + "Tails" + }; + + if let Some(amount) = bet_amount { + if user.balance < amount { + client + .message(format!( + "You don't have enough coins to bet {} coins. You have {}.", + amount, user.balance + )) + .await; + return; + } + + client + .message(format!( + "Coinflipping for {} coins.. I choose {}.", + amount, choice + )) + .await; + + sleep(Duration::from_millis(1750)).await; + + let flip = if rand::rng().random_bool(0.5) { + "Heads" + } else { + "Tails" + }; + + if flip == choice { + let return_coins = (amount as f64 * 1.75).round() as i32; + user.balance += return_coins; + let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2") + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + client + .message(format!("{}! You got {} coins.", flip, return_coins)) + .await; + } else { + user.balance -= amount; + let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2") + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + client + .message(format!("{}! You lost {} coins.", flip, amount)) + .await; + } + } else { + client + .message(format!("Flipping a coin.. I choose {}.", choice)) + .await; + sleep(Duration::from_millis(1750)).await; + + let flip = if rand::rng().random_bool(0.5) { + "Heads" + } else { + "Tails" + }; + let result = if flip == choice { "won" } else { "lost" }; + + client.message(format!("{}! I {}.", flip, result)).await; + } + } +} diff --git a/src/commands/eco/farm.rs b/src/commands/eco/farm.rs new file mode 100644 index 0000000..0a32f91 --- /dev/null +++ b/src/commands/eco/farm.rs @@ -0,0 +1,704 @@ +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use chrono::Utc; +use std::sync::Arc; + +use async_trait::async_trait; +use rand::Rng; +use rand::prelude::IndexedRandom; + +use sqlx::Pool; +use sqlx::Postgres; +use sqlx::Row; + +#[derive(Clone, Copy, Debug)] +pub enum FruitQuality { + OneStar = 1, + TwoStar = 2, + ThreeStar = 3, + FourStar = 4, + FiveStar = 5, +} + +impl FruitQuality { + pub fn stars(&self) -> u8 { + *self as u8 + } + + pub fn name(&self) -> &'static str { + match self { + FruitQuality::OneStar => "⭐", + FruitQuality::TwoStar => "⭐⭐", + FruitQuality::ThreeStar => "⭐⭐⭐", + FruitQuality::FourStar => "⭐⭐⭐⭐", + FruitQuality::FiveStar => "⭐⭐⭐⭐⭐", + } + } +} +#[derive(Debug)] +pub struct FruitType { + pub name: &'static str, + pub color_hex: &'static str, + pub price: u32, +} + +pub struct CropType { + pub name: &'static str, + pub color_hex: &'static str, + pub price: u32, + pub grow_time_minutes: u32, + pub fruits: &'static [FruitType], + pub base_fruit_count: u32, +} + +pub const FRUIT_TYPES: [FruitType; 10] = [ + FruitType { + name: "Beetroot", + color_hex: "#8B0000", + price: 15, + }, + FruitType { + name: "Carrot", + color_hex: "#FF8C00", + price: 12, + }, + FruitType { + name: "Potato", + color_hex: "#D2691E", + price: 8, + }, + FruitType { + name: "Tomato", + color_hex: "#FF6347", + price: 20, + }, + FruitType { + name: "Lettuce", + color_hex: "#90EE90", + price: 18, + }, + FruitType { + name: "Cucumber", + color_hex: "#32CD32", + price: 16, + }, + FruitType { + name: "Apple", + color_hex: "#FF0000", + price: 25, + }, + FruitType { + name: "Pear", + color_hex: "#FFFF00", + price: 22, + }, + FruitType { + name: "Grape", + color_hex: "#8B008B", + price: 30, + }, + FruitType { + name: "Wheat", + color_hex: "#F5DEB3", + price: 10, + }, +]; + +static ROOT_FRUITS: [FruitType; 3] = [ + FruitType { + name: "Beetroot", + color_hex: "#8B0000", + price: 15, + }, + FruitType { + name: "Carrot", + color_hex: "#FF8C00", + price: 12, + }, + FruitType { + name: "Potato", + color_hex: "#D2691E", + price: 8, + }, +]; + +static GARDEN_FRUITS: [FruitType; 3] = [ + FruitType { + name: "Tomato", + color_hex: "#FF6347", + price: 20, + }, + FruitType { + name: "Lettuce", + color_hex: "#90EE90", + price: 18, + }, + FruitType { + name: "Cucumber", + color_hex: "#32CD32", + price: 16, + }, +]; + +static TREE_FRUITS: [FruitType; 3] = [ + FruitType { + name: "Apple", + color_hex: "#FF0000", + price: 25, + }, + FruitType { + name: "Pear", + color_hex: "#FFFF00", + price: 22, + }, + FruitType { + name: "Grape", + color_hex: "#8B008B", + price: 30, + }, +]; + +static GRAIN_FRUITS: [FruitType; 1] = [FruitType { + name: "Wheat", + color_hex: "#F5DEB3", + price: 10, +}]; + +pub const CROP_TYPES: [CropType; 4] = [ + CropType { + name: "Root Vegetables", + color_hex: "#8B4513", + price: 50, + grow_time_minutes: 8, + fruits: &ROOT_FRUITS, + base_fruit_count: 3, + }, + CropType { + name: "Garden Vegetables", + color_hex: "#228B22", + price: 75, + grow_time_minutes: 12, + fruits: &GARDEN_FRUITS, + base_fruit_count: 2, + }, + CropType { + name: "Fruit Trees", + color_hex: "#FF69B4", + price: 150, + grow_time_minutes: 15, + fruits: &TREE_FRUITS, + base_fruit_count: 4, + }, + CropType { + name: "Grain Crops", + color_hex: "#DAA520", + price: 30, + grow_time_minutes: 5, + fruits: &GRAIN_FRUITS, + base_fruit_count: 8, + }, +]; + +pub struct FarmCommand { + pool: Arc>, +} + +impl FarmCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } + + fn get_hoe_bonus(&self, items: &[String]) -> (f64, f64) { + if items.iter().any(|i| i == "diamond_hoe") { + (0.25, 0.30) + } else if items.iter().any(|i| i == "iron_hoe") { + (0.15, 0.20) + } else if items.iter().any(|i| i == "stone_hoe") { + (0.08, 0.10) + } else if items.iter().any(|i| i == "wooden_hoe") { + (0.03, 0.05) + } else { + (0.0, 0.0) + } + } + + fn calculate_fruit_quality(&self, quality_bonus: f64) -> FruitQuality { + let base_roll = rand::rng().random_range(0.0..100.0); + let adjusted_roll = base_roll + (quality_bonus * 100.0); + + if adjusted_roll >= 95.0 { + FruitQuality::FiveStar + } else if adjusted_roll >= 80.0 { + FruitQuality::FourStar + } else if adjusted_roll >= 60.0 { + FruitQuality::ThreeStar + } else if adjusted_roll >= 35.0 { + FruitQuality::TwoStar + } else { + FruitQuality::OneStar + } + } + + pub fn fruit_value(fruit_type: &FruitType, quality: u8, count: u32) -> i32 { + let base_value = fruit_type.price * count; + let quality_multiplier = quality as f32 * 0.5 + 0.5; + (base_value as f32 * quality_multiplier) as i32 + } +} + +#[async_trait] +impl Command for FarmCommand { + fn name(&self) -> &'static str { + "farm" + } + + fn category(&self) -> &'static str { + "eco" + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn description(&self) -> &'static str { + "Plant crops and harvest fruits! Use 'let farm sell' to sell your fruits, or 'let farm harvest' to collect ready crops." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "action", + arg_type: ArgumentType::Enum(&["sell", "all", "harvest"]), + required: false, + default: None, + }] + } + + async fn constructed(&mut self, client: Client) { + let notification_client = client.clone(); + let notification_pool = self.pool.clone(); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(60)); + loop { + interval.tick().await; + + use chrono::Utc; + + let ready_crops = sqlx::query("SELECT DISTINCT _id, type FROM cooldowns WHERE type LIKE 'crop-%' AND untill <= $1") + .bind(Utc::now()) + .fetch_all(notification_pool.as_ref()) + .await; + + if let Ok(crops) = ready_crops { + for crop in crops { + let user_id: String = crop.get("_id"); + let crop_type_name: String = crop.get("type"); + if let Some(crop_name) = crop_type_name.strip_prefix("crop-") + && let Ok(Some(mut user)) = + sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&user_id) + .fetch_optional(notification_pool.as_ref()) + .await + && let Some(crop_type) = CROP_TYPES + .iter() + .find(|c| c.name.to_lowercase().replace(' ', "_") == crop_name) + { + let (quality_bonus, count_bonus) = { + if user.items.iter().any(|i| i == "diamond_hoe") { + (0.25, 0.30) + } else if user.items.iter().any(|i| i == "iron_hoe") { + (0.15, 0.20) + } else if user.items.iter().any(|i| i == "stone_hoe") { + (0.08, 0.10) + } else if user.items.iter().any(|i| i == "wooden_hoe") { + (0.03, 0.05) + } else { + (0.0, 0.0) + } + }; + + let base_roll = rand::rng().random_range(0.0..100.0); + let adjusted_roll = base_roll + (quality_bonus * 100.0); + let quality = if adjusted_roll >= 95.0 { + 5 + } else if adjusted_roll >= 80.0 { + 4 + } else if adjusted_roll >= 60.0 { + 3 + } else if adjusted_roll >= 35.0 { + 2 + } else { + 1 + }; + + let stars = match quality { + 1 => "⭐", + 2 => "⭐⭐", + 3 => "⭐⭐⭐", + 4 => "⭐⭐⭐⭐", + 5 => "⭐⭐⭐⭐⭐", + _ => "⭐", + }; + + let base_count = crop_type.base_fruit_count; + let bonus_count = (base_count as f64 * count_bonus) as u32; + let total_count = + base_count + bonus_count + rand::rng().random_range(1..=5); + + let fruit = crop_type.fruits.choose(&mut rand::rng()).unwrap(); + let fruit_id = format!( + "fruit-{}-{}-{}", + fruit.name.to_lowercase().replace(' ', "_"), + quality, + total_count + ); + + user.items.push(fruit_id); + + let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2") + .bind(&user.items) + .bind(&user._id) + .execute(notification_pool.as_ref()) + .await; + + let _ = + sqlx::query("DELETE FROM cooldowns WHERE _id = $1 AND type = $2") + .bind(&user_id) + .bind(&crop_type_name) + .execute(notification_pool.as_ref()) + .await; + + notification_client + .message(format!( + "🌾 Hey, @{} your {} is ready! You got {} {} x{}", + user_id, crop_type.name, stars, fruit.name, total_count + )) + .await; + } + } + } + } + }); + } + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + .unwrap(); + + let command = match args.get("action") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + if command.eq_ignore_ascii_case("all") { + let mut bought_crops = Vec::new(); + let mut total_cost = 0; + + for crop in &CROP_TYPES { + if user.balance < crop.price as i32 { + continue; + } + + use chrono::Utc; + let crop_id = format!("crop-{}", crop.name.to_lowercase().replace(' ', "_")); + + let existing_crop = sqlx::query_scalar::<_, Option>>( + "SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2", + ) + .bind(&user._id) + .bind(&crop_id) + .fetch_one(self.pool.as_ref()) + .await + .unwrap_or(None); + + if existing_crop.is_some() { + continue; + } + + user.balance -= crop.price as i32; + total_cost += crop.price; + + use chrono::Duration as ChronoDuration; + let ready_time = + Utc::now() + ChronoDuration::minutes(crop.grow_time_minutes as i64); + + let _ = + sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)") + .bind(&user._id) + .bind(&crop_id) + .bind(ready_time) + .execute(self.pool.as_ref()) + .await; + + bought_crops.push(format!( + "{} ({} coins, {} min)", + crop.name, crop.price, crop.grow_time_minutes + )); + } + + if bought_crops.is_empty() { + client + .message( + "You couldn't buy any crops: not enough coins or crops already growing." + .to_string(), + ) + .await; + return; + } + + let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2") + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + client + .message(format!( + "✅ You bought {} crops for a total of {} coins: {}", + bought_crops.len(), + total_cost, + bought_crops.join(", ") + )) + .await; + + return; + } + if command == "sell" { + let mut total_value: i32 = 0; + let mut fruit_count: i32 = 0; + let mut sold_fruits: Vec = Vec::new(); + + let mut new_items: Vec = Vec::new(); + for item in user.items.iter() { + if item.starts_with("fruit-") { + let parts: Vec<&str> = item.split('-').collect(); + if parts.len() >= 4 { + let fruit_name = parts[1].replace('_', " "); + let quality_str = parts[2]; + let count_str = parts[3]; + let quality: u8 = quality_str.parse().unwrap_or(1); + let count: u32 = count_str.parse().unwrap_or(1); + + if let Some(fruit_type) = FRUIT_TYPES + .iter() + .find(|f| f.name.to_lowercase() == fruit_name.to_lowercase()) + { + let value = FarmCommand::fruit_value(fruit_type, quality, count); + total_value += value; + fruit_count += count as i32; + + let stars = match quality { + 1 => "⭐", + 2 => "⭐⭐", + 3 => "⭐⭐⭐", + 4 => "⭐⭐⭐⭐", + 5 => "⭐⭐⭐⭐⭐", + _ => "⭐", + }; + + sold_fruits.push(format!( + "{} {} x{} ({} coins)", + stars, fruit_type.name, count, value + )); + } + } + } else { + new_items.push(item.clone()); + } + } + + if fruit_count == 0 { + client + .message("You have no fruits to sell.".to_string()) + .await; + } else { + user.items = new_items.into(); + user.balance += total_value; + let _ = sqlx::query("UPDATE users SET items = $1, balance = $2 WHERE _id = $3") + .bind(&user.items) + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + client + .message(format!( + "You sold {} fruits for a total of {} coins. {}", + fruit_count, + total_value, + sold_fruits.join(", ") + )) + .await; + } + return; + } + + if command == "harvest" { + let ready_crops = sqlx::query_scalar::<_, String>( + "SELECT type FROM cooldowns WHERE _id = $1 AND type LIKE 'crop-%' AND untill <= $2", + ) + .bind(&user._id) + .bind(Utc::now()) + .fetch_all(self.pool.as_ref()) + .await + .unwrap_or_default(); + + if ready_crops.is_empty() { + client + .message("You have no crops ready for harvest.".to_string()) + .await; + return; + } + + let (quality_bonus, count_bonus) = self.get_hoe_bonus(&user.items); + let mut harvested_items: Vec = Vec::new(); + + for crop_id in ready_crops { + if let Some(crop_name) = crop_id.strip_prefix("crop-") + && let Some(crop_type) = CROP_TYPES + .iter() + .find(|c| c.name.to_lowercase().replace(' ', "_") == crop_name) + { + let quality = self.calculate_fruit_quality(quality_bonus); + let base_count = crop_type.base_fruit_count; + let bonus_count = (base_count as f64 * count_bonus) as u32; + let total_count = base_count + bonus_count + rand::rng().random_range(0..=2); + + let fruit = crop_type.fruits.choose(&mut rand::rng()).unwrap(); + let fruit_id = format!( + "fruit-{}-{}-{}", + fruit.name.to_lowercase().replace(' ', "_"), + quality.stars(), + total_count + ); + + user.items.push(fruit_id.clone()); + harvested_items.push(format!( + "{} {} x{}", + quality.name(), + fruit.name, + total_count + )); + + let _ = sqlx::query("DELETE FROM cooldowns WHERE _id = $1 AND type = $2") + .bind(&user._id) + .bind(&crop_id) + .execute(self.pool.as_ref()) + .await; + } + } + + let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2") + .bind(&user.items) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + client + .message(format!( + "Harvest complete! You got: {}", + harvested_items.join(", ") + )) + .await; + return; + } + + let crop_name = command; + let crop = CROP_TYPES + .iter() + .find(|c| c.name.eq_ignore_ascii_case(crop_name)); + + if let Some(crop) = crop { + if user.balance < crop.price as i32 { + client + .message(format!( + "You don't have enough coins to plant {}. You need {} coins, but you have {}.", + crop.name, crop.price, user.balance + )) + .await; + return; + } + + let crop_id = format!("crop-{}", crop.name.to_lowercase().replace(' ', "_")); + + let existing_crop = sqlx::query_scalar::<_, Option>>( + "SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2", + ) + .bind(&user._id) + .bind(&crop_id) + .fetch_one(self.pool.as_ref()) + .await; + + if let Ok(Some(until)) = existing_crop { + let now = Utc::now(); + if until > now { + let minutes_left = (until - now).num_minutes(); + client + .message(format!( + "You already have {} growing! It will be ready in {} minutes.", + crop.name, minutes_left + )) + .await; + return; + } else { + client + .message(format!( + "{} has fully grown, Use `let farm harvest` to harvest it.", + crop.name + )) + .await; + return; + } + } + + user.balance -= crop.price as i32; + + use chrono::Duration as ChronoDuration; + let ready_time = Utc::now() + ChronoDuration::minutes(crop.grow_time_minutes as i64); + + let _ = sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)") + .bind(&user._id) + .bind(&crop_id) + .bind(ready_time) + .execute(self.pool.as_ref()) + .await; + + let _ = sqlx::query("UPDATE users SET balance = $1 WHERE _id = $2") + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + client + .message(format!( + "You planted {} for {} coins! It will be ready in {} minutes. Use 'let farm harvest' to collect when ready.", + crop.name, crop.price, crop.grow_time_minutes + )) + .await; + } else { + let available_crops = CROP_TYPES + .iter() + .map(|c| { + format!( + "{} ({} coins, {} min)", + c.name, c.price, c.grow_time_minutes + ) + }) + .collect::>() + .join(", "); + client + .message(format!( + "Crop not found. Available crops: {}", + available_crops + )) + .await; + } + } +} diff --git a/src/commands/eco/fish.rs b/src/commands/eco/fish.rs new file mode 100644 index 0000000..adb8664 --- /dev/null +++ b/src/commands/eco/fish.rs @@ -0,0 +1,416 @@ +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::calculate_fish_value; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use chrono::Duration as ChronoDuration; +use chrono::Utc; +use std::sync::Arc; + +use async_trait::async_trait; +use rand::Rng; + +use rand::prelude::IndexedRandom; + +use sqlx::Pool; +use sqlx::Postgres; + +#[derive(Clone, Copy, Debug)] +pub enum FishRarity { + Common, + Uncommon, + Rare, + SuperRare, +} + +pub struct FishType { + pub name: &'static str, + pub color_hex: &'static str, + pub price: u32, + pub rarity: FishRarity, +} + +pub const FISH_TYPES: [FishType; 20] = [ + FishType { + name: "Salmon", + color_hex: "#FA8072", + price: 50, + rarity: FishRarity::Common, + }, + FishType { + name: "Trout", + color_hex: "#A2B5CD", + price: 40, + rarity: FishRarity::Common, + }, + FishType { + name: "Bass", + color_hex: "#6B8E23", + price: 45, + rarity: FishRarity::Common, + }, + FishType { + name: "Catfish", + color_hex: "#B0C4DE", + price: 60, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Carp", + color_hex: "#C2B280", + price: 35, + rarity: FishRarity::Common, + }, + FishType { + name: "Pike", + color_hex: "#556B2F", + price: 70, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Perch", + color_hex: "#FFD700", + price: 30, + rarity: FishRarity::Common, + }, + FishType { + name: "Sturgeon", + color_hex: "#708090", + price: 200, + rarity: FishRarity::Rare, + }, + FishType { + name: "Bluegill", + color_hex: "#4682B4", + price: 25, + rarity: FishRarity::Common, + }, + FishType { + name: "Crappie", + color_hex: "#D3D3D3", + price: 20, + rarity: FishRarity::Common, + }, + FishType { + name: "Swordfish", + color_hex: "#191970", + price: 500, + rarity: FishRarity::SuperRare, + }, + FishType { + name: "Marlin", + color_hex: "#4169E1", + price: 400, + rarity: FishRarity::Rare, + }, + FishType { + name: "Tuna", + color_hex: "#4682B4", + price: 350, + rarity: FishRarity::Rare, + }, + FishType { + name: "Mahi Mahi", + color_hex: "#00CED1", + price: 150, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Halibut", + color_hex: "#F0E68C", + price: 120, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Eel", + color_hex: "#2F4F4F", + price: 80, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Opah", + color_hex: "#FF4500", + price: 600, + rarity: FishRarity::SuperRare, + }, + FishType { + name: "Angelfish", + color_hex: "#FFB6C1", + price: 250, + rarity: FishRarity::Rare, + }, + FishType { + name: "Clownfish", + color_hex: "#FFA500", + price: 180, + rarity: FishRarity::Uncommon, + }, + FishType { + name: "Snapper", + color_hex: "#FF6347", + price: 90, + rarity: FishRarity::Uncommon, + }, +]; + +pub struct FishCommand { + pool: Arc>, +} + +impl FishCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } +} +#[async_trait] +impl Command for FishCommand { + fn name(&self) -> &'static str { + "fish" + } + fn category(&self) -> &'static str { + "eco" + } + fn description(&self) -> &'static str { + "Do some fishing. Try to catch one of many fish types! Rarities and prices included." + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "action", + arg_type: ArgumentType::String, + required: false, + default: None, + }] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + .unwrap(); + + let action = match args.get("action") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => "", + }; + + if action == "sell" { + let mut total_value: i32 = 0; + let mut fish_count: i32 = 0; + let mut sold_fish: Vec = Vec::new(); + + let mut new_items: Vec = Vec::new(); + for item in user.items.iter() { + if item.starts_with("fish-") { + let parts: Vec<&str> = item.split('-').collect(); + if parts.len() >= 3 { + let fish_name = parts[1].replace('_', " "); + let weight_str = parts[2]; + let weight: f64 = weight_str.parse().unwrap_or(0.0); + + if let Some(fish_type) = FISH_TYPES + .iter() + .find(|f| f.name.to_lowercase() == fish_name.to_lowercase()) + { + let value = calculate_fish_value(weight, fish_type.price); + total_value += value; + fish_count += 1; + sold_fish.push(format!( + "{} ({:.2} kg, {} coins)", + fish_type.name, weight, value + )); + } + } + } else { + new_items.push(item.clone()); + } + } + + if fish_count == 0 { + client + .message("You have no fish to sell.".to_string()) + .await; + } else { + user.items = new_items.into(); + user.balance += total_value; + let _ = sqlx::query("UPDATE users SET items = $1, balance = $2 WHERE _id = $3") + .bind(&user.items) + .bind(user.balance) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + client + .message(format!( + "You sold {} fish for a total of {} coins! {}", + fish_count, + total_value, + sold_fish.join(", ") + )) + .await; + } + return; + } + + let cooldown_query_opt = sqlx::query_scalar::<_, Option>>( + "SELECT untill FROM cooldowns WHERE _id = $1 AND type = $2", + ) + .bind(&user._id) + .bind("fish") + .fetch_one(self.pool.as_ref()) + .await; + let now = Utc::now(); + let cooldown_exists = cooldown_query_opt.is_ok(); + if cooldown_exists + && let Some(until) = cooldown_query_opt.unwrap() + && until > now + { + let secs_left = (until - now).num_seconds(); + client + .message(format!( + "⏳ You are tired from fishing! Please wait {} seconds before fishing again.", + secs_left + )) + .await; + return; + } + + let mut rod_bonus = 0.0; + let mut bait_bonus = 0.0; + let mut bait_used = None; + + fn count_item(items: &[String], id: &str) -> usize { + items.iter().filter(|i| i.starts_with(id)).count() + } + + if user.items.iter().any(|i| i == "carbon_rod") { + rod_bonus = 0.15; + } else if user.items.iter().any(|i| i == "fiberglass_rod") { + rod_bonus = 0.07; + } else if user.items.iter().any(|i| i == "wooden_rod") { + rod_bonus = 0.0; + } + + if count_item(&user.items, "large_bait") > 0 { + bait_bonus = 0.10; + bait_used = Some("large_bait"); + } else if count_item(&user.items, "small_bait") > 0 { + bait_bonus = 0.04; + bait_used = Some("small_bait"); + } + + if let Some(bait_id) = bait_used + && let Some(pos) = user.items.iter().position(|i| i.starts_with(bait_id)) + { + user.items.remove(pos); + client + .message(format!("You used one {}.", bait_id.replace('_', " "))) + .await; + } + + let common_fish = FISH_TYPES + .iter() + .filter(|f| matches!(f.rarity, FishRarity::Common)) + .collect::>(); + let uncommon_fish = FISH_TYPES + .iter() + .filter(|f| matches!(f.rarity, FishRarity::Uncommon)) + .collect::>(); + let rare_fish = FISH_TYPES + .iter() + .filter(|f| matches!(f.rarity, FishRarity::Rare)) + .collect::>(); + let super_rare_fish = FISH_TYPES + .iter() + .filter(|f| matches!(f.rarity, FishRarity::SuperRare)) + .collect::>(); + + let mut common_chance = 60.0; + let mut uncommon_chance = 25.0; + let mut rare_chance = 13.0; + let mut super_rare_chance = 2.0; + + let rare_bonus = rod_bonus + bait_bonus; + rare_chance += rare_bonus * 100.0 * 0.5; + super_rare_chance += rare_bonus * 100.0 * 0.5; + let total = common_chance + uncommon_chance + rare_chance + super_rare_chance; + + common_chance *= 100.0 / total; + uncommon_chance *= 100.0 / total; + rare_chance *= 100.0 / total; + + let roll = rand::rng().random_range(0.0..100.0); + let fish = if roll < common_chance { + common_fish.choose(&mut rand::rng()).unwrap() + } else if roll < common_chance + uncommon_chance { + uncommon_fish.choose(&mut rand::rng()).unwrap() + } else if roll < common_chance + uncommon_chance + rare_chance { + rare_fish.choose(&mut rand::rng()).unwrap() + } else { + super_rare_fish.choose(&mut rand::rng()).unwrap() + }; + + let timeout_secs = rand::rng().random_range(24..=48); + + let cooldown_until = now + ChronoDuration::seconds(timeout_secs); + + if cooldown_exists { + let _ = sqlx::query("UPDATE cooldowns SET untill = $1 WHERE _id = $2 AND type = $3") + .bind(cooldown_until) + .bind(&user._id) + .bind("fish") + .execute(self.pool.as_ref()) + .await; + } else { + let _ = sqlx::query("INSERT INTO cooldowns (_id, type, untill) VALUES ($1, $2, $3)") + .bind(&user._id) + .bind("fish") + .bind(cooldown_until) + .execute(self.pool.as_ref()) + .await; + } + + let base_weight = rand::rng().random_range(0.1..=50.0); + let weight = base_weight * (1.0 + rod_bonus + bait_bonus); + + let fish_id = format!( + "fish-{}-{:.2}", + fish.name.to_lowercase().replace(' ', "_"), + weight + ); + + user.items.push(fish_id.clone()); + let _ = sqlx::query("UPDATE users SET items = $1 WHERE _id = $2") + .bind(&user.items) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await; + + let value = calculate_fish_value(weight, fish.price); + + client + .message(format!( + "🎣 You caught a {}! Color: {} Weight: {:.2} kg", + fish.name, fish.color_hex, weight + )) + .await; + client + .message(format!("Rarity: {:?} Value: {} coins", fish.rarity, value)) + .await; + let client_clone = client.clone(); + tokio::spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(timeout_secs as u64)).await; + client_clone + .message(format!("🎣 @{} can fish again now!", player._id)) + .await; + }); + } +} diff --git a/src/commands/eco/inv.rs b/src/commands/eco/inv.rs new file mode 100644 index 0000000..e2dee87 --- /dev/null +++ b/src/commands/eco/inv.rs @@ -0,0 +1,153 @@ +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::eco::fish::FISH_TYPES; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use std::collections::HashMap; +use std::sync::Arc; + +use async_trait::async_trait; +use sqlx::Pool; +use sqlx::Postgres; + +pub struct InventoryCommand { + pool: Arc>, +} + +impl InventoryCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } +} + +#[async_trait] +impl Command for InventoryCommand { + fn name(&self) -> &'static str { + "inventory" + } + fn category(&self) -> &'static str { + "eco" + } + fn aliases(&self) -> &[&'static str] { + &["inv"] + } + fn description(&self) -> &'static str { + "View your inventory." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "user_id", + arg_type: ArgumentType::String, + required: false, + default: None, + }] + } + + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let id = match args.get("user_id") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => player._id.as_str(), + }; + + let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap(); + + if let Some(user) = user { + if user.items.is_empty() { + client.message("Your inventory is empty.".to_string()).await; + return; + } + + let mut fruits: Vec<(u8, String)> = Vec::new(); + let mut fish: Vec = Vec::new(); + let mut others: HashMap = HashMap::new(); + + for item in user.items.iter() { + if item.starts_with("fruit-") { + let parts: Vec<&str> = item.split('-').collect(); + if parts.len() >= 4 { + let mut fruit_name = parts[1].replace('_', " "); + let quality: u8 = parts[2].parse().unwrap_or(1); + let count: u32 = parts[3].parse().unwrap_or(1); + + let stars = match quality { + 1 => "⭐", + 2 => "⭐⭐", + 3 => "⭐⭐⭐", + 4 => "⭐⭐⭐⭐", + 5 => "⭐⭐⭐⭐⭐", + _ => "⭐", + }; + fruits.push(( + quality, + format!( + "{} {}{fruit_name} x{}", + stars, + fruit_name.remove(0).to_uppercase(), + count + ), + )); + } + } else if item.starts_with("fish-") { + let parts: Vec<&str> = item.split('-').collect(); + if parts.len() >= 3 { + let fish_name = parts[1].replace('_', " "); + let weight: f64 = parts[2].parse().unwrap_or(0.0); + + if let Some(fish_type) = FISH_TYPES + .iter() + .find(|f| f.name.eq_ignore_ascii_case(&fish_name)) + { + fish.push(format!("{} ({:.2} kg)", fish_type.name, weight)); + } else { + fish.push(format!("{} ({:.2} kg)", fish_name, weight)); + } + } + } else { + let display_name = match item.as_str() { + "wooden_rod" => "Wooden Rod".to_string(), + "fiberglass_rod" => "Fiberglass Rod".to_string(), + "carbon_rod" => "Carbon Rod".to_string(), + "small_bait" => "Small Bait".to_string(), + "large_bait" => "Large Bait".to_string(), + "wooden_hoe" => "Wooden Hoe".to_string(), + "iron_hoe" => "Iron Hoe".to_string(), + "golden_hoe" => "Golden Hoe".to_string(), + "diamond_hoe" => "Diamond Hoe".to_string(), + _ => item.replace('_', " "), + }; + *others.entry(display_name).or_insert(0) += 1; + } + } + + fruits.sort_by(|a, b| b.0.cmp(&a.0)); + + let mut inventory_display = Vec::new(); + + inventory_display.extend(fruits.into_iter().map(|(_, s)| s)); + inventory_display.extend(fish); + inventory_display.extend(others.into_iter().map(|(name, count)| { + if count > 1 { + format!("{} x{}", name, count) + } else { + name + } + })); + + client + .message(format!("Inventory: {}", inventory_display.join(", "))) + .await; + } else { + client.message("No user found.".to_string()).await; + } + } +} diff --git a/src/commands/eco/mod.rs b/src/commands/eco/mod.rs new file mode 100644 index 0000000..5bddf7e --- /dev/null +++ b/src/commands/eco/mod.rs @@ -0,0 +1,15 @@ +use crate::submods; + +submods!(balance, fish, farm, shop, inv, cf); + +pub fn calculate_fish_value(weight: f64, price: u32) -> i32 { + let min_weight = 0.1; + let max_weight = 50.0; + let min_multiplier = 1.0; + let max_multiplier = 2.5; + + let normalized_weight = ((weight - min_weight) / (max_weight - min_weight)).clamp(0.0, 1.0); + let multiplier = min_multiplier + (max_multiplier - min_multiplier) * normalized_weight; + + (price as f64 * multiplier).round() as i32 +} diff --git a/src/commands/eco/shop.rs b/src/commands/eco/shop.rs new file mode 100644 index 0000000..8b6f3ae --- /dev/null +++ b/src/commands/eco/shop.rs @@ -0,0 +1,209 @@ +use crate::User; +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; + +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use std::sync::Arc; + +use async_trait::async_trait; +use sqlx::Pool; +use sqlx::Postgres; + +pub struct ShopItem { + pub name: &'static str, + pub price: i32, + pub max: u8, + pub category: &'static str, + pub id: &'static str, +} + +pub const SHOP_ITEMS: [ShopItem; 9] = [ + ShopItem { + name: "Wooden Rod", + price: 100, + category: "Fishing Rod", + id: "wooden_rod", + max: 1, + }, + ShopItem { + name: "Fiberglass Rod", + price: 500, + category: "Fishing Rod", + id: "fiberglass_rod", + max: 1, + }, + ShopItem { + name: "Carbon Rod", + price: 2000, + category: "Fishing Rod", + id: "carbon_rod", + max: 1, + }, + ShopItem { + name: "Small Bait", + price: 20, + category: "Bait", + id: "small_bait", + max: 100, + }, + ShopItem { + name: "Large Bait", + price: 80, + category: "Bait", + id: "large_bait", + max: 100, + }, + ShopItem { + name: "Wooden Hoe", + price: 80, + category: "Farming Tool", + id: "wooden_hoe", + max: 1, + }, + ShopItem { + name: "Stone Hoe", + price: 200, + category: "Farming Tool", + id: "stone_hoe", + max: 1, + }, + ShopItem { + name: "Iron Hoe", + price: 800, + category: "Farming Tool", + id: "iron_hoe", + max: 1, + }, + ShopItem { + name: "Diamond Hoe", + price: 3000, + category: "Farming Tool", + id: "diamond_hoe", + max: 1, + }, +]; + +pub struct ShopCommand { + pool: Arc>, +} + +impl ShopCommand { + pub fn new(pool: Arc>) -> Self { + Self { pool } + } +} +#[async_trait] +impl Command for ShopCommand { + fn name(&self) -> &'static str { + "shop" + } + fn category(&self) -> &'static str { + "eco" + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn description(&self) -> &'static str { + "Do some shopping." + } + + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ + ArgumentSpec { + name: "item_id", + arg_type: ArgumentType::String, + required: false, + default: None, + }, + ArgumentSpec { + name: "quantity", + arg_type: ArgumentType::Integer, + required: false, + default: Some("1"), + }, + ] + } + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let mut user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(self.pool.as_ref()) + .await + .unwrap() + .unwrap(); + + let item_name = match args.get("item_id") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => "empty", + }; + + let quantity: u8 = match args.get("quantity") { + Some(ParsedArgument::Integer(i)) => *i as u8, + _ => 1, + }; + + let item = SHOP_ITEMS + .iter() + .find(|i| i.id.eq_ignore_ascii_case(item_name)); + + if let Some(item) = item { + let owned_count = user.items.iter().filter(|id| id == &item.id).count() as u8; + if owned_count >= item.max { + client + .message(&format!( + "You already own the maximum amount of {} ({}).", + item.name, item.max + )) + .await; + return; + } + + let quantity = quantity.min(item.max - owned_count); + + let total_price = item.price * quantity as i32; + + if user.balance >= total_price { + user.balance -= total_price; + for _ in 0..quantity { + user.items.push(item.id.to_string()); + } + sqlx::query("UPDATE users SET balance = $1, items = $2 WHERE _id = $3") + .bind(user.balance as i64) + .bind(&user.items) + .bind(&user._id) + .execute(self.pool.as_ref()) + .await + .unwrap(); + + client + .message(&format!( + "You purchased {}x {} ({}) for {} coins!", + quantity, item.name, item.category, total_price + )) + .await; + } else { + client.message(&format!( + "You don't have enough coins to buy {}x {}. You need {} coins, but you have {}.", + quantity, item.name, total_price, user.balance + )).await; + } + } else { + let available_items = SHOP_ITEMS + .iter() + .map(|i| format!("{} ({})", i.name, i.id)) + .collect::>() + .join(", "); + client + .message(&format!( + "Item not found. Available items: {}", + available_items + )) + .await; + } + } +} diff --git a/src/commands/midi/mod.rs b/src/commands/midi/mod.rs new file mode 100644 index 0000000..a7e8ee4 --- /dev/null +++ b/src/commands/midi/mod.rs @@ -0,0 +1,190 @@ +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, +}; + +use flume::{Receiver, Sender}; +use tokio::{sync::Mutex, task::JoinHandle}; + +use crate::{ + client::Client, + midi_helper::{MidiEvent, play_midi}, + submods, +}; + +submods!(play, playlist, queue, skip, stop); + +pub fn number_to_midi() -> HashMap { + let mut map = HashMap::new(); + map.insert(1, "a-1"); + map.insert(2, "as-1"); + map.insert(3, "b-1"); + map.insert(4, "c0"); + map.insert(5, "cs0"); + map.insert(6, "d0"); + map.insert(7, "ds0"); + map.insert(8, "e0"); + map.insert(9, "f0"); + map.insert(10, "fs0"); + map.insert(11, "g0"); + map.insert(12, "gs0"); + map.insert(13, "a0"); + map.insert(14, "as0"); + map.insert(15, "b0"); + map.insert(16, "c1"); + map.insert(17, "cs1"); + map.insert(18, "d1"); + map.insert(19, "ds1"); + map.insert(20, "e1"); + map.insert(21, "f1"); + map.insert(22, "fs1"); + map.insert(23, "g1"); + map.insert(24, "gs1"); + map.insert(25, "a1"); + map.insert(26, "as1"); + map.insert(27, "b1"); + map.insert(28, "c2"); + map.insert(29, "cs2"); + map.insert(30, "d2"); + map.insert(31, "ds2"); + map.insert(32, "e2"); + map.insert(33, "f2"); + map.insert(34, "fs2"); + map.insert(35, "g2"); + map.insert(36, "gs2"); + map.insert(37, "a2"); + map.insert(38, "as2"); + map.insert(39, "b2"); + map.insert(40, "c3"); + map.insert(41, "cs3"); + map.insert(42, "d3"); + map.insert(43, "ds3"); + map.insert(44, "e3"); + map.insert(45, "f3"); + map.insert(46, "fs3"); + map.insert(47, "g3"); + map.insert(48, "gs3"); + map.insert(49, "a3"); + map.insert(50, "as3"); + map.insert(51, "b3"); + map.insert(52, "c4"); + map.insert(53, "cs4"); + map.insert(54, "d4"); + map.insert(55, "ds4"); + map.insert(56, "e4"); + map.insert(57, "f4"); + map.insert(58, "fs4"); + map.insert(59, "g4"); + map.insert(60, "gs4"); + map.insert(61, "a4"); + map.insert(62, "as4"); + map.insert(63, "b4"); + map.insert(64, "c5"); + map.insert(65, "cs5"); + map.insert(66, "d5"); + map.insert(67, "ds5"); + map.insert(68, "e5"); + map.insert(69, "f5"); + map.insert(70, "fs5"); + map.insert(71, "g5"); + map.insert(72, "gs5"); + map.insert(73, "a5"); + map.insert(74, "as5"); + map.insert(75, "b5"); + map.insert(76, "c6"); + map.insert(77, "cs6"); + map.insert(78, "d6"); + map.insert(79, "ds6"); + map.insert(80, "e6"); + map.insert(81, "f6"); + map.insert(82, "fs6"); + map.insert(83, "g6"); + map.insert(84, "gs6"); + map.insert(85, "a6"); + map.insert(86, "as6"); + map.insert(87, "b6"); + map.insert(88, "c7"); + map +} + +pub struct MidiState { + pub midi_tx: Sender, + pub midi_rx: Receiver, + pub midi_handle: Option>, + pub queue: Arc>>, +} + +impl Default for MidiState { + fn default() -> Self { + Self::new() + } +} + +impl MidiState { + pub fn new() -> Self { + let (midi_tx, midi_rx) = flume::unbounded::(); + Self { + midi_tx, + midi_rx, + midi_handle: None, + queue: Arc::new(Mutex::new(VecDeque::new())), + } + } +} + +pub fn simple_hash(s: &str) -> u64 { + let mut hash: u64 = 5381; + for b in s.bytes() { + hash = ((hash << 5).wrapping_add(hash)).wrapping_add(b as u64); + } + hash +} + +pub async fn play_midi_file( + filename_to_play: String, + filename_beautiful: String, + only_queue: bool, + midi_state: Arc>, + client: Client, +) { + let midi_tx; + let queue; + { + let midi_state = midi_state.lock().await; + midi_tx = midi_state.midi_tx.clone(); + queue = midi_state.queue.clone(); + } + + let handle_filename_to_play = filename_to_play.clone(); + let handle_filename_beautiful = filename_beautiful.clone(); + let handle_client = client.clone(); + + let midi_handle = tokio::spawn(async move { + let next_mtx = midi_tx.clone(); + if !only_queue { + let _ = play_midi(handle_filename_to_play.as_str(), midi_tx).await; + handle_client + .message(format!("{} ended.", handle_filename_beautiful)) + .await; + } + loop { + let mut locked_queue = queue.lock().await; + if locked_queue.is_empty() { + drop(locked_queue); + break; + } + let midi = locked_queue.pop_front().unwrap(); + let queue_len = locked_queue.len(); + drop(locked_queue); + + handle_client + .message(format!("Queue left: {}, playing {}.", queue_len, midi.0)) + .await; + + let _ = play_midi(midi.1.as_str(), next_mtx.clone()).await; + } + }); + + let mut midi_state = midi_state.lock().await; + midi_state.midi_handle = Some(midi_handle); +} diff --git a/src/commands/midi/play.rs b/src/commands/midi/play.rs new file mode 100644 index 0000000..5b1328c --- /dev/null +++ b/src/commands/midi/play.rs @@ -0,0 +1,263 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Note; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::MidiState; +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArguments}; +use crate::commands::number_to_midi; +use crate::commands::play_midi_file; +use crate::commands::simple_hash; +use crate::midi_helper::MidiEvent; + +use async_trait::async_trait; + +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct PlayCommand { + midi_state: Arc>, +} + +impl PlayCommand { + pub fn new(midi_state: Arc>) -> Self { + Self { midi_state } + } +} + +#[async_trait] +impl Command for PlayCommand { + fn name(&self) -> &'static str { + "play" + } + fn aliases(&self) -> &[&'static str] { + &["p"] + } + fn category(&self) -> &'static str { + "midi" + } + fn description(&self) -> &'static str { + "plays a midi file from a local path, URL, or files.sad.ovh" + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "file", + arg_type: ArgumentType::String, + required: true, + default: None, + }] + } + + async fn constructed(&mut self, client: Client) { + let ntm = number_to_midi(); + let midi_rx = { + let midi_state = self.midi_state.lock().await; + midi_state.midi_rx.clone() + }; + + tokio::spawn(async move { + while let Ok(event) = midi_rx.recv_async().await { + match event { + MidiEvent::NoteOn { key, velocity } => { + client + .add_note_buffer(Note { + n: ntm.get(&key).unwrap_or(&"").to_string(), + v: Some(velocity as f64 / 127.0), + d: None, + s: None, + }) + .await; + } + MidiEvent::NoteOff { key } => { + client + .add_note_buffer(Note { + n: ntm.get(&key).unwrap_or(&"").to_string(), + v: None, + d: None, + s: Some(1), + }) + .await; + } + MidiEvent::Info { + num_tracks, + time_div: _, + events_count, + note_count, + total_ticks: _, + minutes, + seconds, + millis, + parse_time, + } => { + client + .message(format!( + "Tracks: `{}` Events: `{}` Total Duration: `{:02}:{:02}.{:03}` Note Count: `{}` Parse time: `{:.2?}`", + num_tracks, events_count, minutes, seconds, millis, note_count, parse_time + )) + .await; + } + } + } + }); + } + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + let file_arg = match args.get("file") { + Some(crate::commands::argument::ParsedArgument::String(s)) => s, + _ => "", + }; + + let joined_args = file_arg.to_string(); + let mut filename_to_play: String = "".to_string(); + let mut filename_beautiful: String = "".to_string(); + + if joined_args.starts_with("https://") { + let hashed = simple_hash(joined_args.as_str()); + filename_to_play = format!("midis/{}.mid", hashed); + filename_beautiful = joined_args.clone(); + + let file_exists = tokio::fs::try_exists(&filename_to_play) + .await + .unwrap_or(false); + if !file_exists { + match reqwest::get(&joined_args).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.bytes().await { + Ok(bytes) => { + match tokio::fs::write(&filename_to_play, &bytes).await { + Ok(_) => { + client + .message(format!( + "Downloaded midi from {}, into: {}", + joined_args, filename_to_play + )) + .await; + } + Err(e) => { + client + .message(format!("Failed to write file: {}", e)) + .await; + return; + } + } + } + Err(e) => { + client + .message(format!("Failed to read response bytes: {}", e)) + .await; + return; + } + } + } else { + client + .message(format!("Failed to download file: HTTP {}", resp.status())) + .await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to download file: {}", e)) + .await; + return; + } + } + } + } else if joined_args.starts_with("files:") { + if let Some(caps) = joined_args.strip_prefix("files:") { + let first_capture = caps.trim(); + + if first_capture.ends_with('/') { + client + .message("Use the 'playlist' command to play a directory as a playlist.") + .await; + return; + } else { + let hashed = simple_hash(first_capture); + filename_to_play = format!("midis/{}.mid", hashed); + filename_beautiful = first_capture.to_string(); + + let url = format!("https://files.sad.ovh/public/midis/{}", first_capture); + + let file_exists = tokio::fs::try_exists(&filename_to_play) + .await + .unwrap_or(false); + if !file_exists { + match reqwest::get(&url).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.bytes().await { + Ok(bytes) => { + match tokio::fs::write(&filename_to_play, &bytes).await + { + Ok(_) => { + client + .message(format!( + "Downloaded {} from files.sad.ovh.", + filename_beautiful + )) + .await; + } + Err(e) => { + client + .message(format!( + "Failed to write file: {}", + e + )) + .await; + return; + } + } + } + Err(e) => { + client + .message(format!( + "Failed to read response bytes: {}", + e + )) + .await; + return; + } + } + } else { + client + .message(format!( + "Failed to download file: HTTP {}", + resp.status() + )) + .await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to download file: {}", e)) + .await; + return; + } + } + } + } + } + } else { + filename_to_play = joined_args.clone(); + filename_beautiful = joined_args.clone(); + } + + play_midi_file( + filename_to_play, + filename_beautiful.clone(), + false, + self.midi_state.clone(), + client.clone(), + ) + .await; + + client + .message(format!("Started playing {}.", filename_beautiful)) + .await; + } +} diff --git a/src/commands/midi/playlist.rs b/src/commands/midi/playlist.rs new file mode 100644 index 0000000..b465d11 --- /dev/null +++ b/src/commands/midi/playlist.rs @@ -0,0 +1,362 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; + +use crate::commands::Command; +use crate::commands::MidiState; +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use crate::commands::simple_hash; +use crate::play_midi_file; +use rand::seq::SliceRandom; + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct PlaylistCommand { + midi_state: Arc>, +} + +impl PlaylistCommand { + pub fn new(midi_state: Arc>) -> Self { + Self { midi_state } + } +} + +#[async_trait] +impl Command for PlaylistCommand { + fn name(&self) -> &'static str { + "playlist" + } + fn category(&self) -> &'static str { + "midi" + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn description(&self) -> &'static str { + "Loads and plays a playlist of midis." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "playlist", + arg_type: ArgumentType::String, + required: true, + default: None, + }] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + let joined_args = match args.get("playlist") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => "", + }; + + let mut filename_to_play: String = "".to_string(); + let mut filename_beautiful: String = "".to_string(); + + if joined_args == "playlist_1" { + const TRACKS: &[&str] = &[ + "A/autumn_leaves2-G85.mid", + "B/blue_bossa-kenny-dorham_dz.mid", + "J/Jobim_Desafinado.mid", + "J/Jobim_Wave.mid", + "J/Jobim_corcovado.mid", + "I/ipanema.mid", + "J/Jobim_Meditacao2.mid", + "S/so_what.mid", + "T/take_five2-Gb176-davebrubeck.mid", + "M/My_Funny_Valentine.mid", + "A/all_the_things_you_are-2_dm.mid", + "J/Johnny_Mathis_Misty.mid", + "H/HOWARD.Fly me to the moon.mid", + "R/Ray Charles - Georgia On My Mind.mid", + "B/blue_in_green.mid", + "S/Stella-By-Starlight-1.mid", + "F/FITZGERALD.Summertime K.mid", + "M/Moon River.mid", + "A/autumn_in_new_york2-Bb84.mid", + "J/Jobim_One_Note_Samba.mid", + ]; + + for track in TRACKS { + let hashed = simple_hash(track); + let filename = format!("midis/{}.mid", hashed); + let url = format!("https://files.sad.ovh/public/midis/{}", track); + + let file_exists = tokio::fs::try_exists(&filename).await.unwrap_or(false); + if !file_exists { + match reqwest::get(&url).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.bytes().await { + Ok(bytes) => match tokio::fs::write(&filename, &bytes).await { + Ok(_) => { + client + .message(format!( + "Downloaded midi from files.sad.ovh: {}", + track + )) + .await; + } + Err(e) => { + client + .message(format!("Failed to write file: {}", e)) + .await; + } + }, + Err(e) => { + client + .message(format!( + "Failed to read response bytes: {}", + e + )) + .await; + } + } + } else { + client + .message(format!( + "Failed to download file: HTTP {} for {}", + resp.status(), + track + )) + .await; + } + } + Err(e) => { + client + .message(format!("Failed to download file: {} for {}", e, track)) + .await; + } + } + } + } + + let mut tracks: Vec<&str> = TRACKS.to_vec(); + tracks.shuffle(&mut rand::rng()); + + if let Some(first) = tracks.first() { + let hashed = simple_hash(first); + filename_to_play = format!("midis/{}.mid", hashed); + + filename_beautiful = first.to_string(); + + let midi_state = self.midi_state.clone(); + let locked_state = midi_state.lock().await; + + let mut locked_queue = locked_state.queue.lock().await; + + for track in tracks.iter().skip(1) { + let hashed = simple_hash(track); + let filename = format!("midis/{}.mid", hashed); + locked_queue.push_back((track.to_string(), filename)); + } + drop(locked_queue); + drop(locked_state); + client + .message(format!( + "Playlist loaded. Playing '{}', queued {} more.", + filename_beautiful, + tracks.len() - 1 + )) + .await; + } else { + client.message("No tracks found in playlist.").await; + return; + } + } else if joined_args.starts_with("files:") + && let Some(caps) = joined_args.strip_prefix("files:") + { + let first_capture = caps.trim(); + + if first_capture.ends_with('/') { + let url = format!("https://files.sad.ovh/public/midis/{}?ls", first_capture); + match reqwest::get(&url).await { + Ok(resp) => { + if resp.status().is_success() { + match resp.text().await { + Ok(text) => { + let parsed: serde_json::Value = + match serde_json::from_str(&text) { + Ok(val) => val, + Err(e) => { + client + .message(format!( + "Failed to parse directory listing JSON: {}", + e + )) + .await; + return; + } + }; + let files = parsed.get("files").and_then(|f| f.as_array()); + if let Some(files) = files { + if files.is_empty() { + client.message("No files found in directory.").await; + return; + } + let mut file_entries: Vec<(String, String)> = Vec::new(); + for file in files { + let href = file.get("href").and_then(|h| h.as_str()); + if let Some(href) = href { + let file_path = + format!("{}{}", first_capture, href); + let hashed = simple_hash(&file_path); + let filename = format!("midis/{}.mid", hashed); + let url_file = format!( + "https://files.sad.ovh/public/midis/{}", + file_path + ); + + let file_exists = tokio::fs::try_exists(&filename) + .await + .unwrap_or(false); + if !file_exists { + match reqwest::get(&url_file).await { + Ok(resp_file) => { + if resp_file.status().is_success() { + match resp_file.bytes().await { + Ok(bytes) => { + match tokio::fs::write( + &filename, &bytes, + ) + .await + { + Ok(_) => { + client + .message(format!( + "Downloaded {} from files.sad.ovh.", + file_path + )) + .await; + } + Err(e) => { + client + .message(format!("Failed to write file: {}", e)) + .await; + return; + } + } + } + Err(e) => { + client + .message(format!( + "Failed to read response bytes: {}", + e + )) + .await; + return; + } + } + } else { + client + .message(format!( + "Failed to download file: HTTP {}", + resp_file.status() + )) + .await; + return; + } + } + Err(e) => { + client + .message(format!( + "Failed to download file: {}", + e + )) + .await; + return; + } + } + } + file_entries.push((file_path.clone(), filename)); + } + } + if file_entries.is_empty() { + client + .message("No valid files found in directory.") + .await; + return; + } + + filename_beautiful = file_entries[0].0.clone(); + filename_to_play = file_entries[0].1.clone(); + + let midi_state = self.midi_state.clone(); + let locked_state = midi_state.lock().await; + let queued_count = file_entries.len() - 1; + { + let mut locked_queue = locked_state.queue.lock().await; + for entry in file_entries.iter().skip(1) { + locked_queue + .push_back((entry.0.clone(), entry.1.clone())); + } + } + + client + .message(format!( + "Directory loaded. Playing '{}', queued {} more.", + filename_beautiful, queued_count + )) + .await; + } else { + client + .message("No files found in directory listing.") + .await; + return; + } + } + Err(e) => { + client + .message(format!( + "Failed to read directory listing response: {}", + e + )) + .await; + return; + } + } + } else { + client + .message(format!( + "Failed to get directory listing: HTTP {}", + resp.status() + )) + .await; + return; + } + } + Err(e) => { + client + .message(format!("Failed to get directory listing: {}", e)) + .await; + return; + } + } + } else { + client + .message("Use the 'play' command to play a single file.") + .await; + return; + } + } + + play_midi_file( + filename_to_play, + filename_beautiful.clone(), + false, + self.midi_state.clone(), + client.clone(), + ) + .await; + + client + .message(format!("Started playing {}.", filename_beautiful)) + .await; + } +} diff --git a/src/commands/midi/queue.rs b/src/commands/midi/queue.rs new file mode 100644 index 0000000..41c1e15 --- /dev/null +++ b/src/commands/midi/queue.rs @@ -0,0 +1,62 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::MidiState; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; +pub struct QueueCommand { + midi_state: Arc>, +} + +impl QueueCommand { + pub fn new(midi_state: Arc>) -> Self { + Self { midi_state } + } +} + +#[async_trait] +impl Command for QueueCommand { + fn name(&self) -> &'static str { + "queue" + } + fn category(&self) -> &'static str { + "midi" + } + fn aliases(&self) -> &[&'static str] { + &["q"] + } + fn description(&self) -> &'static str { + "Shows the current midi queue." + } + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + let midi_state = self.midi_state.lock().await; + let locked_queue = midi_state.queue.lock().await; + let queue_len = locked_queue.len(); + if queue_len == 0 { + client.message("Queue is empty.").await; + } else { + let midis: Vec = locked_queue.iter().cloned().map(|z| z.0).collect(); + let midis_list = midis.join(", "); + client + .message(format!( + "Queue length: {}. Queued midis: {}", + queue_len, midis_list + )) + .await; + } + drop(locked_queue); + } +} diff --git a/src/commands/midi/skip.rs b/src/commands/midi/skip.rs new file mode 100644 index 0000000..ec99df6 --- /dev/null +++ b/src/commands/midi/skip.rs @@ -0,0 +1,66 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::MidiState; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; +use crate::commands::play_midi_file; + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; +pub struct SkipCommand { + midi_state: Arc>, +} + +impl SkipCommand { + pub fn new(midi_state: Arc>) -> Self { + Self { midi_state } + } +} + +#[async_trait] +impl Command for SkipCommand { + fn name(&self) -> &'static str { + "skip" + } + fn category(&self) -> &'static str { + "midi" + } + fn aliases(&self) -> &[&'static str] { + &["s"] + } + fn description(&self) -> &'static str { + "Skips the current midi and plays the next in the queue." + } + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + let mut midi_state = self.midi_state.lock().await; + if let Some(handle) = midi_state.midi_handle.as_ref() + && !handle.is_finished() + { + handle.abort(); + midi_state.midi_handle = None; + client.message("Skipped current midi.").await; + drop(midi_state); + play_midi_file( + "".to_string(), + "".to_string(), + true, + self.midi_state.clone(), + client, + ) + .await; + return; + } + client.message("Nothing is playing to skip.").await; + } +} diff --git a/src/commands/midi/stop.rs b/src/commands/midi/stop.rs new file mode 100644 index 0000000..a7f73ae --- /dev/null +++ b/src/commands/midi/stop.rs @@ -0,0 +1,57 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::{ArgumentSpec, Command, MidiState, ParsedArguments}; + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct StopCommand { + midi_state: Arc>, +} + +impl StopCommand { + pub fn new(midi_state: Arc>) -> Self { + Self { midi_state } + } +} + +#[async_trait] +impl Command for StopCommand { + fn name(&self) -> &'static str { + "stop" + } + fn category(&self) -> &'static str { + "midi" + } + fn aliases(&self) -> &[&'static str] { + &["x"] + } + fn description(&self) -> &'static str { + "Stops the current midi playback and clears the queue." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, _args: ParsedArguments) { + let mut midi_state = self.midi_state.lock().await; + if let Some(handle) = midi_state.midi_handle.as_ref() + && !handle.is_finished() + { + handle.abort(); + midi_state.midi_handle = None; + let mut locked_queue = midi_state.queue.lock().await; + locked_queue.clear(); + drop(locked_queue); + client.message("Stopped playing (forcefully).").await; + return; + } + client.message("Nothing is playing.").await; + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..0bb68ca --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,87 @@ +use crate::{ + client::{Client, ClientEvent, Player}, + commands::argument::{ArgumentSpec, ParsedArguments}, +}; +use async_trait::async_trait; +use std::{collections::HashMap, sync::Arc}; +use tokio::sync::Mutex; + +#[macro_export] +macro_rules! submods { + ($($name:ident),*) => { + $( + pub mod $name; + pub use $name::*; + )* + }; +} + +submods!(eco, midi, system); +pub mod argument; + +pub struct Arguments { + pub args: Vec, +} + +impl Arguments { + pub fn new(args: Vec) -> Self { + Self { args } + } + + pub fn get(&self, index: usize) -> Option<&str> { + self.args.get(index).map(|s| s.as_str()) + } +} +#[async_trait] +pub trait Command: Send + Sync { + fn name(&self) -> &'static str; + fn description(&self) -> &'static str; + fn category(&self) -> &'static str; + fn aliases(&self) -> &[&'static str]; + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + + async fn event(&mut self, client: Client, event: ClientEvent); + + async fn constructed(&mut self, client: Client); + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments); +} + +pub type CommandArc = Arc>; +pub type CommandValues<'a> = std::collections::hash_map::Values<'a, String, CommandArc>; + +#[derive(Clone)] +pub struct CommandRegistry { + commands: HashMap, +} + +impl Default for CommandRegistry { + fn default() -> Self { + Self::new() + } +} + +impl CommandRegistry { + pub fn new() -> Self { + Self { + commands: HashMap::new(), + } + } + + pub fn values(&self) -> CommandValues<'_> { + self.commands.values() + } + + pub async fn register(&mut self, mut cmd: C, c: Client) { + cmd.constructed(c).await; // await async setup + self.commands.insert( + cmd.name().to_string(), + Arc::new(Mutex::new(cmd)) as Arc>, + ); + } + + pub fn get(&self, name: &str) -> Option>> { + self.commands.get(name).cloned() + } +} diff --git a/src/commands/system/about.rs b/src/commands/system/about.rs new file mode 100644 index 0000000..543ad49 --- /dev/null +++ b/src/commands/system/about.rs @@ -0,0 +1,60 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::argument::ArgumentType; +use crate::commands::argument::ParsedArgument; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; + +use async_trait::async_trait; + +pub struct AboutCommand; + +#[async_trait] +impl Command for AboutCommand { + fn name(&self) -> &'static str { + "about" + } + fn category(&self) -> &'static str { + "system" + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn description(&self) -> &'static str { + "View information about this bot." + } + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "section", + arg_type: ArgumentType::Enum(&["midi", "arguments"]), + required: false, + default: None, + }] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + let section = match args.get("section") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + match section { + "" => { + client.message("Copper is a MPP bot written in Rust. It supports very high NPS midis, and a very exhaustive economy system. View `about midi` and `about arguments` for more information. - Sophie, created 08/23/2025, 1:10 AM").await; + } + "midi" => { + client.message("The midi player in this bot is written by Austin (Nitsua). Currently closed source, it's memory usage is 1:1 of the midis size, only in loading the midi. The midi is required to load fully to get the size of it, but after that it uses basically no ram. I've played many midis while using less than 1MB of ram.").await; + } + "arguments" => { + client.message("The argument system here is very complicated. All commands that have arguments have a ArgumentSpec list, which I can add arguments into. These have `required`, defaults and argument types. There are 5 argument types. Floats, integers, enums, strings and greedyStrings.").await; + } + _ => {} + }; + } +} diff --git a/src/commands/system/follow.rs b/src/commands/system/follow.rs new file mode 100644 index 0000000..f9530fb --- /dev/null +++ b/src/commands/system/follow.rs @@ -0,0 +1,162 @@ +use std::f64::consts::PI; +use std::sync::Arc; +use std::time::Duration; + +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; + +use crate::commands::Command; +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArgument, ParsedArguments}; +use async_trait::async_trait; +use tokio::sync::Mutex; +use tokio::task::JoinHandle; +use tokio::time::sleep; + +pub struct FollowCommand { + orbit_center: Arc>, + animation: Arc>, + player: String, + handle: Option>, +} + +impl Default for FollowCommand { + fn default() -> Self { + Self::new() + } +} + +impl FollowCommand { + pub fn new() -> Self { + Self { + orbit_center: Arc::new(Mutex::new((50.0, 50.0))), + animation: Arc::new(Mutex::new("default".to_string())), + player: "".to_string(), + handle: None, + } + } +} + +#[async_trait] +impl Command for FollowCommand { + fn name(&self) -> &'static str { + "follow" + } + fn aliases(&self) -> &[&'static str] { + &["f"] + } + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "Follow a person." + } + async fn event(&mut self, _: Client, event: ClientEvent) { + if let ClientEvent::Mouse { x, y, id } = event + && id == self.player + { + let mut lock = self.orbit_center.lock().await; + *lock = (x, y); + } + } + async fn constructed(&mut self, client: Client) { + let cloned_orbit_center = self.orbit_center.clone(); + let cloned_animation = self.animation.clone(); + + self.handle = Some(tokio::spawn(async move { + // non_snake_case is set here because this is code ported from js + // and to make the whole process easier, i keep botLength in non-snake-case + #[allow(non_snake_case)] + let botLength = 5.0; + let speed = 1.0; + let mut theta: f64 = 0.0; + let mut last = tokio::time::Instant::now(); + + loop { + let now = tokio::time::Instant::now(); + let dt = now.duration_since(last).as_secs_f64(); + last = now; + + theta += speed * dt; + let mut x = 0.0; + let mut y = 0.0; + let i = 5.0; + + let (cx, cy) = { *cloned_orbit_center.lock().await }; + let animation = { cloned_animation.lock().await.clone() }; + + if animation == "default" { + let t = ((PI * 2.0) / botLength) * i - theta; + //let t1 = ((PI * 3.0) / botLength) * i + theta; + let x1 = cx + (f64::cos(2.0 * t) * botLength) / 2.0; + let y1 = cy + (f64::sin(2.0 * t) * botLength) / 2.0; + x = x1 + (f64::cos(3.0 * t) * botLength) / 2.0; + y = y1 + (f64::sin(3.0 * t) * botLength) / 2.0; + } + + let _ = client.move_to(x, y).await; + + sleep(Duration::from_millis(50)).await; // ~60 fps + } + })); + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ + ArgumentSpec { + name: "target", + arg_type: ArgumentType::String, + required: false, + default: None, + }, + ArgumentSpec { + name: "animation", + arg_type: ArgumentType::Enum(&["default"]), + required: false, + default: None, + }, + ] + } + + async fn execute(&mut self, client: Client, player: Player, args: ParsedArguments) { + let target = match args.get("target") { + Some(ParsedArgument::String(s)) => s.as_str(), + _ => player._id.as_str(), + }; + + let animation = match args.get("animation") { + Some(ParsedArgument::Enum(s)) => s.clone(), + _ => "default".to_string(), + }; + + if target == "off" { + if self.handle.is_none() { + client.message("Follow handle is already gone.").await; + return; + } + client + .message( + "Dismantled the follow handle. Rerun `follow` to enable following once again.", + ) + .await; + self.handle.as_mut().unwrap().abort(); + self.handle = None; + + return; + } + + { + let mut lock = self.animation.lock().await; + *lock = animation; + } + + self.player = target.to_string(); + + if self.handle.is_none() { + self.constructed(client.clone()).await; + } + + client + .message(format!("Now following {}.", self.player)) + .await; + } +} diff --git a/src/commands/system/help.rs b/src/commands/system/help.rs new file mode 100644 index 0000000..20a80f9 --- /dev/null +++ b/src/commands/system/help.rs @@ -0,0 +1,111 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::CommandRegistry; +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArguments}; + +use async_trait::async_trait; +use std::collections::BTreeMap; + +pub struct HelpCommand { + registry: CommandRegistry, +} + +impl HelpCommand { + pub fn new(registry: CommandRegistry) -> Self { + Self { registry } + } +} + +#[async_trait] +impl Command for HelpCommand { + fn name(&self) -> &'static str { + "help" + } + fn aliases(&self) -> &[&'static str] { + &["?"] + } + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "View all commands." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "command", + arg_type: ArgumentType::String, + required: false, + default: None, + }] + } + async fn event(&mut self, _: Client, _: ClientEvent) {} + async fn constructed(&mut self, _: Client) {} + + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + let cmd_name = match args.get("command") { + Some(crate::commands::argument::ParsedArgument::String(s)) if !s.is_empty() => s, + _ => "", + }; + if !cmd_name.is_empty() + && let Some(cmd_mutex) = self.registry.commands.get(cmd_name) + { + let cmd = cmd_mutex.lock().await; + let message = { + let mut arg_string: Vec = Vec::new(); + + for arg in cmd.argument_spec() { + let argument_brackets = { + if arg.required { + ("(", "*)") + } else { + ("(", ")") + } + }; + /*let default_text = { + if arg.default.is_some() { + format!("= {}", arg.default.unwrap()) + } else { + "".to_string() + } + };*/ + + arg_string.push(format!( + "{}{}{}", + argument_brackets.0, arg.name, argument_brackets.1 + )); + } + + format!( + "`{} {}` - {} *Required", + cmd_name, + arg_string.join(" ").trim(), + cmd.description() + ) + }; + + client.message(message).await; + return; + } + + let mut categories: BTreeMap> = BTreeMap::new(); + + for (name, cmd_mutex) in self.registry.commands.iter() { + let cmd = cmd_mutex.lock().await; + categories + .entry(cmd.category().to_string()) + .or_default() + .push(name.clone()); + } + + for (category, cmds) in categories.iter() { + let cmds_list = cmds + .iter() + .map(|c| format!("`{}`", c)) + .collect::>() + .join(", "); + client.message(format!("{}: {}", category, cmds_list)).await; + } + } +} diff --git a/src/commands/system/launch.rs b/src/commands/system/launch.rs new file mode 100644 index 0000000..d6ce1e5 --- /dev/null +++ b/src/commands/system/launch.rs @@ -0,0 +1,53 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::argument::{ArgumentSpec, ArgumentType, ParsedArguments}; + +use async_trait::async_trait; + +pub struct LaunchCommand; + +#[async_trait] +impl Command for LaunchCommand { + fn name(&self) -> &'static str { + "launch" + } + fn aliases(&self) -> &[&'static str] { + &["goto"] + } + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "Launches the bot to somewhere else." + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ArgumentSpec { + name: "channel", + arg_type: ArgumentType::String, + required: true, + default: None, + }] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, mut client: Client, player: Player, args: ParsedArguments) { + if player._id != "3bff3f33e6dc0410fdc61d13" { + client + .message("You are not `3bff3f33e6dc0410fdc61d13`.") + .await; + } + let channel = match args.get("channel") { + Some(crate::commands::argument::ParsedArgument::String(s)) => s, + _ => "", + }; + client.message(format!("Going to `{}`.", channel)).await; + client.channel = channel.to_string(); + + client + .send_command(serde_json::json!({"m": "ch", "_id": client.channel})) + .await; + } +} diff --git a/src/commands/system/mod.rs b/src/commands/system/mod.rs new file mode 100644 index 0000000..661db96 --- /dev/null +++ b/src/commands/system/mod.rs @@ -0,0 +1,3 @@ +use crate::submods; + +submods!(follow, help, launch, test, translate, about); diff --git a/src/commands/system/test.rs b/src/commands/system/test.rs new file mode 100644 index 0000000..8ff8f88 --- /dev/null +++ b/src/commands/system/test.rs @@ -0,0 +1,45 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; + +use crate::commands::Command; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; +use async_trait::async_trait; + +pub struct TestCommand {} + +impl Default for TestCommand { + fn default() -> Self { + Self::new() + } +} + +impl TestCommand { + pub fn new() -> Self { + Self {} + } +} +#[async_trait] +impl Command for TestCommand { + fn name(&self) -> &'static str { + "test" + } + fn category(&self) -> &'static str { + "system" + } + fn description(&self) -> &'static str { + "Ping." + } + fn aliases(&self) -> &[&'static str] { + &[] + } + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[] + } + async fn constructed(&mut self, _: Client) {} + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, _: ParsedArguments) { + client.message("Ping.").await; + } +} diff --git a/src/commands/system/translate.rs b/src/commands/system/translate.rs new file mode 100644 index 0000000..2ea01df --- /dev/null +++ b/src/commands/system/translate.rs @@ -0,0 +1,158 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::client::Player; +use crate::commands::Command; +use crate::commands::argument::ArgumentType; +use crate::commands::argument::ParsedArgument; +use crate::commands::argument::{ArgumentSpec, ParsedArguments}; + +// Libretranslate implementation. +use async_trait::async_trait; +use reqwest::Client as ReqwestClient; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Serialize)] +struct TranslateRequest<'a> { + q: &'a str, + source: &'a str, + target: &'a str, + format: &'a str, +} + +#[derive(Deserialize)] +struct TranslateResponse { + #[serde(rename = "translatedText")] + translated_text: String, +} + +pub struct LibreTranslate { + client: ReqwestClient, + url: String, +} + +impl LibreTranslate { + pub fn new(base_url: &str) -> Self { + LibreTranslate { + client: ReqwestClient::new(), + url: base_url.to_string(), + } + } + + pub async fn translate( + &self, + text: &str, + source_lang: &str, + target_lang: &str, + ) -> Result> { + let request_body = TranslateRequest { + q: text, + source: source_lang, + target: target_lang, + format: "text", + }; + let response = self + .client + .post(format!("{}/translate", self.url)) + .header("Content-Type", "application/json") + .body(serde_json::to_string(&request_body).unwrap_or("".to_string())) + .send() + .await; + + if let Ok(ok_response) = response { + let result: TranslateResponse = + serde_json::from_str(ok_response.text().await.unwrap().as_str()).unwrap(); + Ok(result.translated_text) + } else { + Err(format!( + "API request failed with status: {}", + response.err().unwrap().status().unwrap() + ) + .into()) + } + } +} +// Libretranslate implementation end. + +pub struct TranslateCommand; + +#[async_trait] +impl Command for TranslateCommand { + fn name(&self) -> &'static str { + "translate" + } + fn category(&self) -> &'static str { + "system" + } + fn aliases(&self) -> &[&'static str] { + &["trans"] + } + fn description(&self) -> &'static str { + "Translate from a language to a language." + } + + fn argument_spec(&self) -> &'static [ArgumentSpec] { + &[ + ArgumentSpec { + name: "to_language", + arg_type: ArgumentType::Enum(&["ru", "lv", "en", "ko"]), + required: true, + default: None, + }, + ArgumentSpec { + name: "from_language", + arg_type: ArgumentType::Enum(&["ru", "lv", "en", "ko", "auto"]), + required: true, + default: None, + }, + ArgumentSpec { + name: "text", + arg_type: ArgumentType::GreedyString, + required: true, + default: None, + }, + ] + } + + async fn constructed(&mut self, _client: Client) {} + + async fn event(&mut self, _: Client, _: ClientEvent) {} + + async fn execute(&mut self, client: Client, _: Player, args: ParsedArguments) { + let to_language = match args.get("to_language") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + let from_language = match args.get("from_language") { + Some(ParsedArgument::Enum(s)) => s.as_str(), + _ => "", + }; + + let text = match args.get("text") { + Some(ParsedArgument::GreedyString(s)) => s.as_str(), + _ => "", + }; + + client.message("Okay, translating your message.").await; + let text = text.to_string(); + let to_language = to_language.to_string(); + let from_language = from_language.to_string(); + let tokio_client = client.clone(); + + tokio::spawn(async move { + let translate = LibreTranslate::new("https://lt.sad.ovh"); + let translated = translate + .translate(&text, &from_language, &to_language) + .await; + tokio_client + .message(format!( + "`{}` in `{}` is: `{}`", + text, + to_language, + translated.unwrap() + )) + .await; + }); + } +} diff --git a/src/log.rs b/src/log.rs new file mode 100644 index 0000000..52f5178 --- /dev/null +++ b/src/log.rs @@ -0,0 +1,114 @@ +pub const RESET: &str = "\x1b[0m"; +pub const GREEN: &str = "\x1b[32m"; +pub const YELLOW: &str = "\x1b[33m"; +pub const RED: &str = "\x1b[31m"; +pub const BLUE: &str = "\x1b[34m"; +pub const CYAN: &str = "\x1b[36m"; +pub const MAGENTA: &str = "\x1b[35m"; + +pub fn pad_prefix(prefix: &str, width: usize) -> String { + let padding = if prefix.len() >= width { + 1 + } else { + width - prefix.len() + }; + format!("{}{}", prefix, " ".repeat(padding)) +} + +pub const MAX_PREFIX_WIDTH: usize = 18; + +#[macro_export] +macro_rules! log { + (JOINED, $channel:expr, $visible:expr, $actual:expr) => { + use $crate::log::*; + println!( + "{}{}{} | Channel {}{}{} | People: {} (actual: {})", + GREEN, + pad_prefix("[JOINED]", MAX_PREFIX_WIDTH), + RESET, + CYAN, + $channel, + RESET, + $visible, + $actual + ); + }; + (DISCONNECTED) => { + use $crate::log::*; + + println!( + "{}{}{} | WebSocket closed, reconnecting in 1 second...", + YELLOW, + pad_prefix("[DISCONNECTED]", MAX_PREFIX_WIDTH), + RESET + ); + }; + (ERRORED, $e:expr) => { + use $crate::log::*; + println!( + "{}{}{} | WebSocket error: {}, reconnecting in 1 second...", + RED, + pad_prefix("[ERROR]", MAX_PREFIX_WIDTH), + RESET, + $e + ); + }; + (CONNECTED) => { + use $crate::log::*; + println!( + "{}{}{} | Connected!", + GREEN, + pad_prefix("[CONNECTED]", MAX_PREFIX_WIDTH), + RESET + ); + }; + (MSG, $user:expr, $message:expr) => { + use $crate::log::*; + println!( + "{}{}{} | {}{}{}: {}", + BLUE, + pad_prefix("[MSG]", MAX_PREFIX_WIDTH), + RESET, + MAGENTA, + $user, + RESET, + $message + ); + }; + (PLAYER_JOINED, $player:expr) => { + use $crate::log::*; + println!( + "{}{}{} | {}{}{} joined", + GREEN, + pad_prefix("[PLAYER JOINED]", MAX_PREFIX_WIDTH), + RESET, + CYAN, + $player, + RESET + ); + }; + (PLAYER_LEFT, $player:expr) => { + use $crate::log::*; + println!( + "{}{}{} | {}{}{} left", + YELLOW, + pad_prefix("[PLAYER LEFT]", MAX_PREFIX_WIDTH), + RESET, + CYAN, + $player, + RESET + ); + }; + (CHANNEL_INFO, $channel:expr) => { + use $crate::log::*; + println!( + "{}{}{} | Channel name: {}{}{}", + CYAN, + pad_prefix("[CHANNEL INFO]", MAX_PREFIX_WIDTH), + RESET, + MAGENTA, + $channel, + RESET + ); + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e99181f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,218 @@ +use crate::client::Client; +use crate::client::ClientEvent; +use crate::commands::Arguments; +use crate::commands::CommandRegistry; +use cap::Cap; +use serde::Deserialize; +use sqlx::postgres::PgPoolOptions; +use tokio::sync::Mutex; + +pub mod client; +pub mod commands; +pub mod log; +pub mod midi_helper; + +use crate::commands::*; + +use std::sync::Arc; + +#[global_allocator] +static ALLOCATOR: Cap = Cap::new(std::alloc::System, usize::MAX); + +#[derive(sqlx::FromRow)] +struct User { + _id: String, + balance: i32, + items: sqlx::types::Json>, +} + +macro_rules! register_all { + ($registry:expr, $client:expr, [ $($cmd:expr),+ $(,)? ]) => { + $( + $registry.register($cmd, $client.clone()).await; + )+ + }; +} +#[derive(Deserialize)] +struct Configuration { + database: DatabaseConfig, + commands: CommandsConfig, + client: ClientConfig, +} + +#[derive(Deserialize)] +struct DatabaseConfig { + url: String, +} + +#[derive(Deserialize)] +struct CommandsConfig { + prefix: String, + name: String, +} + +#[derive(Deserialize)] +struct ClientConfig { + token: String, + ws: String, + room: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + rustls::crypto::CryptoProvider::install_default(rustls_rustcrypto::provider()) + .expect("install rustcrypto provider"); + + let s = tokio::fs::read_to_string("config.hocon").await?; + let conf: Configuration = hocon::de::from_str(&s)?; + + let (mut client, event_rx) = Client::new(); + let pool = PgPoolOptions::new() + .max_connections(5) + .connect(conf.database.url.as_str()) + .await?; + let arc_pool = Arc::new(pool); + + let midi_state = Arc::new(Mutex::new(MidiState::new())); + + let mut registry = CommandRegistry::new(); + + register_all!( + registry, + client, + [ + PlayCommand::new(midi_state.clone()), + StopCommand::new(midi_state.clone()), + PlaylistCommand::new(midi_state.clone()), + QueueCommand::new(midi_state.clone()), + SkipCommand::new(midi_state), + LaunchCommand, + FollowCommand::new(), + TestCommand::new(), + BalanceCommand::new(arc_pool.clone()), + InventoryCommand::new(arc_pool.clone()), + FishCommand::new(arc_pool.clone()), + FarmCommand::new(arc_pool.clone()), + ShopCommand::new(arc_pool.clone()), + CoinflipCommand::new(arc_pool.clone()), + TranslateCommand, + AboutCommand, + HelpCommand::new(registry.clone()), + ] + ); + + let client_events = client.clone(); + let events_pool = arc_pool.clone(); + + tokio::spawn(async move { + while let Ok(event) = event_rx.recv_async().await { + match event { + ClientEvent::Connected => { + log!(CONNECTED); + } + ClientEvent::Sync { e } => { + let t = e - chrono::Utc::now().timestamp_millis(); + + let username = { + let ram = ALLOCATOR.allocated() / 1_000_000; + if ram != 0 { + format!("{} 📶{} ms 🐏{} mb ", conf.commands.name, t, ram) + } else { + format!("{} 📶{} ms", conf.commands.name, -t) + } + }; + + let _ = client_events + .userset(username.as_str().into(), "#B7410E".into()) + .await; + } + ClientEvent::Message { player, message } => { + if let Some(no_prefix) = message.strip_prefix(conf.commands.prefix.as_str()) { + let mut parts = no_prefix.split_whitespace(); + if let Some(cmd_name) = parts.next() { + let args = Arguments::new(parts.map(|s| s.to_string()).collect()); + + let mut cmd_opt: Option = None; + for cmd in registry.values() { + let cmd_lock = cmd.lock().await; + if cmd_lock.name() == cmd_name + || cmd_lock.aliases().contains(&cmd_name) + { + cmd_opt = Some(cmd.clone()); + break; + } + } + + if let Some(cmd) = cmd_opt { + let mut cmd_lock = cmd.lock().await; + let specs = cmd_lock.argument_spec(); + match crate::commands::argument::parse_arguments(specs, &args.args) + { + Ok(parsed_args_struct) => { + cmd_lock + .execute( + client_events.clone(), + player.clone(), + parsed_args_struct, + ) + .await; + } + Err(e) => { + client_events + .message(format!("Argument error: {}", e)) + .await; + } + } + } + } + } + + if message == "!ping" { + client_events.message("pong").await; + } + log!(MSG, player.name, message); + } + ClientEvent::PlayerJoined(player) => { + log!(PLAYER_JOINED, player.name); + + let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE _id = $1") + .bind(&player._id) + .fetch_optional(events_pool.as_ref()) + .await; + + let user_exists = user.unwrap().is_some(); + + if !user_exists { + let _ = sqlx::query!( + "INSERT INTO users (_id, balance, items) VALUES ($1, $2, $3)", + player.id, + 0, + serde_json::json!([]) + ) + .execute(events_pool.as_ref()) + .await; + } + } + ClientEvent::PlayerLeft(id) => { + log!(PLAYER_LEFT, id); + } + ClientEvent::Mouse { x, y, id } => { + let cmd_lock = registry.get("follow").unwrap(); + let mut cmd = cmd_lock.lock().await; + cmd.event(client_events.clone(), ClientEvent::Mouse { x, y, id }) + .await; + } + ClientEvent::NameChanged { now: _, before: _ } => {} + ClientEvent::ChannelInfo(channel) => { + log!(CHANNEL_INFO, channel.id); + } + } + } + }); + + client + .connect(conf.client.ws, conf.client.token, conf.client.room) + .await?; + + Ok(()) +} diff --git a/src/midi_helper.rs b/src/midi_helper.rs new file mode 100644 index 0000000..d06d7af --- /dev/null +++ b/src/midi_helper.rs @@ -0,0 +1,131 @@ +use flume::Sender; +use midiplayer_rs::midi::loader::load_midi_file; +use midiplayer_rs::midi::player::parse_midi_events; +use midiplayer_rs::midi::player::play_parsed_events; +use std::time::Duration; +use std::time::Instant; +use thousands::Separable; + +#[derive(Debug, Clone)] +pub enum MidiEvent { + NoteOn { + key: u8, + velocity: u8, + }, + NoteOff { + key: u8, + }, + Info { + num_tracks: String, + time_div: u16, + events_count: String, + note_count: String, + total_ticks: String, + minutes: u128, + seconds: u128, + millis: u128, + parse_time: Duration, + }, +} + +pub fn delay_execution_100ns_blocking(delay_in_100ns: i64) { + if delay_in_100ns <= 0 { + return; + } + + let secs = delay_in_100ns / 10_000_000; + let nanos = (delay_in_100ns % 10_000_000) * 100; + + let duration = std::time::Duration::new(secs as u64, nanos as u32); + std::thread::sleep(duration); +} + +pub async fn play_midi( + path: &str, + tx: Sender, +) -> Result<(), Box> { + let (tracks, time_div) = load_midi_file(path).unwrap(); + let num_tracks = tracks.len(); + + let start = Instant::now(); + let parsed = parse_midi_events(tracks, time_div, 0); + let total_ms = parsed.total_duration.as_millis(); + let minutes = total_ms / 60_000; + let seconds = (total_ms % 60_000) / 1_000; + let millis = total_ms % 1_000; + + let _ = tx + .send_async(MidiEvent::Info { + num_tracks: num_tracks.separate_with_commas(), + time_div, + events_count: parsed.events.len().separate_with_commas(), + note_count: parsed.note_count.separate_with_commas(), + total_ticks: parsed.total_ticks.separate_with_commas(), + minutes, + seconds, + millis, + parse_time: start.elapsed(), + }) + .await; + + let c_tx = tx.clone(); + + tokio::task::spawn_blocking(move || { + play_parsed_events( + &parsed, + time_div, + move |data| { + let status = (data & 0xFF) as u8; + let data1 = ((data >> 8) & 0xFF) as u8; + let data2 = ((data >> 16) & 0xFF) as u8; + + let status_type = status & 0xF0; + let channel = (status & 0x0F) + 1; + + match status_type { + 0x90 => { + let key_index = data1.saturating_sub(21); + if key_index < 88 { + if data2 != 0 { + let _ = c_tx.send(MidiEvent::NoteOn { + key: key_index, + velocity: data2, + }); + } else { + let _ = c_tx.send(MidiEvent::NoteOff { key: key_index }); + } + } + } + 0x80 => { + let key_index = data1.saturating_sub(21); + if key_index < 88 { + let _ = c_tx.send(MidiEvent::NoteOff { key: key_index }); + } + } + 0xA0 => println!( + "[DECODE] Polyphonic Aftertouch - ch={} note={} pressure={}", + channel, data1, data2 + ), + 0xB0 => println!( + "[DECODE] Control Change - ch={} controller={} value={}", + channel, data1, data2 + ), + 0xC0 => println!("[DECODE] Program Change - ch={} program={}", channel, data1), + 0xD0 => println!( + "[DECODE] Channel Pressure - ch={} pressure={}", + channel, data1 + ), + 0xE0 => { + let pitch_bend = ((data2 as u16) << 7) | (data1 as u16); + println!("[DECODE] Pitch Bend - ch={} value={}", channel, pitch_bend); + } + _ => println!("[WARN] Unknown/Unsupported MIDI message 0x{:02X}", status), + } + }, + Some(Box::new(|ns: i64| delay_execution_100ns_blocking(ns))), + ); + }) + .await?; + + Ok(()) +}