diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..354f683 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,49 @@ +permissions: + contents: write + +name: Release + +on: + push: + tags: + - "v*" + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v3 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-msvc + + - name: Install cargo-about + run: cargo install cargo-about + + - name: Build + run: cargo build --release + + - name: Generate License + run: cargo about generate about.hbs -o ThirdPartyLicenses.html + + - name: Create Release Archive + shell: bash + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + REPO_NAME=${GITHUB_REPOSITORY##*/} + RELEASE_NAME="${REPO_NAME}-${TAG_NAME}-windows" + + mkdir -p "${RELEASE_NAME}" + cp "target/release/${REPO_NAME}.exe" "${RELEASE_NAME}/" + cp LICENSE README.md ThirdPartyLicenses.html "${RELEASE_NAME}/" + 7z a "${RELEASE_NAME}.zip" "${RELEASE_NAME}" + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: "*.zip" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52d431e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +appdata +BOT_TOKEN +bep-eng.dic +settings.json +/deny.toml \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f6b69e2 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4136 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[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 = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + +[[package]] +name = "anyhow" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +dependencies = [ + "serde", +] + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "audiopus" +version = "0.3.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab55eb0e56d7c6de3d59f544e5db122d7725ec33be6a276ee8241f3be6473955" +dependencies = [ + "audiopus_sys", +] + +[[package]] +name = "audiopus_sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651" +dependencies = [ + "cmake", + "log", + "pkg-config", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "bytecount" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" + +[[package]] +name = "bytemuck" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b37c88a63ffd85d15b406896cc343916d7cf57838a847b3a6f2ca5d39a5695a" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[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.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[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 = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "command_attr" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88da8d7e9fe6f30d8e3fcf72d0f84102b49de70fece952633e8439e89bdc7631" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.52.0", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.6.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix", + "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-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling", + "derive_builder_core", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.89", +] + +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "discortp" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c793408a15d361754613fa68123ffa60424c2617fafdf82127b4bedf37d3f5d" +dependencies = [ + "pnet_macros", + "pnet_macros_support", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[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 = "engtokana" +version = "0.0.0" +dependencies = [ + "anyhow", + "log", + "reqwest 0.12.8", + "tokio", +] + +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "eq_uilibrium" +version = "0.1.0" +source = "git+https://github.com/aq2r/eq-uilibrium?tag=v0.1.0#c809c8f56eba9d842987256ac4c86cdead7f83d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "extended" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +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 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "from_map" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f31122ab0445ff8cee420b805f24e07683073815de1dd276ee7d588d301700" +dependencies = [ + "hashmap_derive", +] + +[[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-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.89", +] + +[[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 = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[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 = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hashmap_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb30bf173e72cc31b5265dac095423ca14e7789ff7c3b0e6096a37a996f12883" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hls_m3u8" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b906521a5b0e6d2ec07ea0bb855d92a1db30b48812744a645a3b2a1405cb8159" +dependencies = [ + "derive_builder", + "derive_more", + "hex", + "shorthand", + "stable-vec", + "strum", + "thiserror", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.31", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.5.0", + "hyper-util", + "rustls 0.23.15", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.5.0", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.5.0", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "langrustang" +version = "1.1.4" +source = "git+https://github.com/aq2r/langrustang?tag=v1.1.4#b9e4a91cb335388b698a5362f90aea6f3166533d" +dependencies = [ + "anyhow", + "pretty_assertions", + "proc-macro2", + "quote", + "serde", + "serde_yaml", + "syn 2.0.89", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "levenshtein" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db13adb97ab515a3691f56e4dbab09283d0b86cb45abd991d8634a9d6f501760" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "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 = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "log", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "no-std-compat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df270209a7f04d62459240d890ecb792714d5db12c92937823574a09930276b4" + +[[package]] +name = "no-std-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[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-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "patricia_tree" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f2f4539bffe53fc4b4da301df49d114b845b077bd5727b7fe2bd9d8df2ae68" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be57f64e946e500c8ee36ef6331845d40a93055567ec57e8fae13efd33759b95" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c0f5fad0874fc7abcd4d750e76917eaebbecaa2c20bde22e1dbeeba8beb758c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "pnet_base" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" +dependencies = [ + "no-std-net", +] + +[[package]] +name = "pnet_macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn 2.0.89", +] + +[[package]] +name = "pnet_macros_support" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" +dependencies = [ + "pnet_base", +] + +[[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 = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primal-check" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro2" +version = "1.0.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" +dependencies = [ + "bitflags 2.6.0", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "realfft" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390252372b7f2aac8360fc5e72eba10136b166d6faeed97e6d0c8324eb99b2b1" +dependencies = [ + "rustfft", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.31", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.5.0", + "hyper-rustls 0.27.3", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile 2.2.0", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ringbuf" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726bb493fe9cac765e8f96a144c3a8396bdf766dedad22e504b70b908dcbceb4" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "rubato" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d18b486e7d29a408ef3f825bc1327d8f87af091c987ca2f5b734625940e234" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "realfft", +] + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags 2.6.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustfft" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43806561bc506d0c5d160643ad742e3161049ac01027b5e6d7524091fd401d86" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "rustix" +version = "0.38.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.20.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" +dependencies = [ + "log", + "ring 0.16.20", + "sct", + "webpki", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "rusty_pool" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed36cdb20de66d89a17ea04b8883fc7a386f2cf877aaedca5005583ce4876ff" +dependencies = [ + "crossbeam-channel", + "futures", + "futures-channel", + "futures-executor", + "num_cpus", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "sbv2_api" +version = "0.0.0" +dependencies = [ + "anyhow", + "reqwest 0.12.8", + "serde_json", + "tokio", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-aux" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d2e8bfba469d06512e11e3311d4d051a4a387a5b42d010404fecf3200321c95" +dependencies = [ + "chrono", + "serde", + "serde_json", +] + +[[package]] +name = "serde-value" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" +dependencies = [ + "ordered-float", + "serde", +] + +[[package]] +name = "serde_cow" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7bbbec7196bfde255ab54b65e34087c0849629280028238e67ee25d6a4b7da" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.215" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[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 = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serenity" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "880a04106592d0a8f5bdacb1d935889bfbccb4a14f7074984d9cd857235d34ac" +dependencies = [ + "arrayvec", + "async-trait", + "base64 0.22.1", + "bitflags 2.6.0", + "bytes", + "chrono", + "command_attr", + "dashmap", + "flate2", + "futures", + "fxhash", + "levenshtein", + "mime_guess", + "parking_lot", + "percent-encoding", + "reqwest 0.11.27", + "secrecy", + "serde", + "serde_cow", + "serde_json", + "static_assertions", + "time", + "tokio", + "tokio-tungstenite 0.21.0", + "tracing", + "typemap_rev", + "typesize", + "url", + "uwl", +] + +[[package]] +name = "serenity-voice-model" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "593682f6155d07c8b331b3d1060f5aab7e6796caca9f2f66bd9e6855c880e06b" +dependencies = [ + "bitflags 2.6.0", + "num-traits", + "serde", + "serde_json", + "serde_repr", +] + +[[package]] +name = "setting_inputter" +version = "0.0.0" +dependencies = [ + "anyhow", + "crossterm", + "dialoguer", + "log", + "serde", + "serde_json", + "tokio", +] + +[[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 = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "shorthand" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474f77f985d8212610f170332eaf173e768404c0c1d4deb041f32c297cf18931" +dependencies = [ + "from_map", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "signal-hook" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" +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", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "songbird" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0faa1cb1580cb87e12ece4474e6d84a25f588afd3ab0c6e05feb3805850d60c2" +dependencies = [ + "aead", + "aes-gcm", + "async-trait", + "audiopus", + "byteorder", + "bytes", + "chacha20poly1305", + "crypto_secretbox", + "dashmap", + "derivative", + "discortp", + "flume", + "futures", + "nohash-hasher", + "once_cell", + "parking_lot", + "pin-project", + "rand", + "reqwest 0.11.27", + "ringbuf", + "rubato", + "rusty_pool", + "serde", + "serde-aux", + "serde_json", + "serenity", + "serenity-voice-model", + "socket2", + "stream_lib", + "streamcatcher", + "symphonia", + "symphonia-core", + "tokio", + "tokio-tungstenite 0.21.0", + "tokio-util", + "tracing", + "tracing-futures", + "twilight-gateway", + "typemap_rev", + "typenum", + "url", + "uuid", +] + +[[package]] +name = "sonorust" +version = "0.0.0" +dependencies = [ + "anyhow", + "engtokana", + "eq_uilibrium", + "langrustang", + "log", + "regex", + "sbv2_api", + "serenity", + "setting_inputter", + "songbird", + "sonorust_db", + "sonorust_logger", + "symphonia", + "tokio", +] + +[[package]] +name = "sonorust_db" +version = "0.0.0" +dependencies = [ + "anyhow", + "rusqlite", + "serenity", + "tokio", +] + +[[package]] +name = "sonorust_logger" +version = "0.0.0" +dependencies = [ + "chrono", + "env_logger", + "log", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable-vec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1dff32a2ce087283bec878419027cebd888760d8760b2941ad0843531dc9ec8" +dependencies = [ + "no-std-compat", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "stream_lib" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3f10eb5a7054e17abf61d310e4e29108187a847591c63c4c79b6a74898a5a7" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "hls_m3u8", + "patricia_tree", + "reqwest 0.11.27", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "streamcatcher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71664755c349abb0758fda6218fb2d2391ca2a73f9302c03b145491db4fcea29" +dependencies = [ + "crossbeam-utils", + "futures-util", + "loom", +] + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + +[[package]] +name = "strum" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6e163a520367c465f59e0a61a23cfae3b10b6546d78b6f672a382be79f7110" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symphonia" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" +dependencies = [ + "lazy_static", + "symphonia-bundle-flac", + "symphonia-bundle-mp3", + "symphonia-codec-aac", + "symphonia-codec-adpcm", + "symphonia-codec-alac", + "symphonia-codec-pcm", + "symphonia-codec-vorbis", + "symphonia-core", + "symphonia-format-isomp4", + "symphonia-format-mkv", + "symphonia-format-ogg", + "symphonia-format-riff", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-bundle-flac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-bundle-mp3" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-codec-aac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-adpcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94e1feac3327cd616e973d5be69ad36b3945f16b06f19c6773fc3ac0b426a0f" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-alac" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d8a6666649a08412906476a8b0efd9b9733e241180189e9f92b09c08d0e38f3" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-pcm" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b" +dependencies = [ + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-codec-vorbis" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" +dependencies = [ + "log", + "symphonia-core", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-core" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" +dependencies = [ + "arrayvec", + "bitflags 1.3.2", + "bytemuck", + "lazy_static", + "log", +] + +[[package]] +name = "symphonia-format-isomp4" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844" +dependencies = [ + "encoding_rs", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-mkv" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bb43471a100f7882dc9937395bd5ebee8329298e766250b15b3875652fe3d6f" +dependencies = [ + "lazy_static", + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-ogg" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", + "symphonia-utils-xiph", +] + +[[package]] +name = "symphonia-format-riff" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50" +dependencies = [ + "extended", + "log", + "symphonia-core", + "symphonia-metadata", +] + +[[package]] +name = "symphonia-metadata" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" +dependencies = [ + "encoding_rs", + "lazy_static", + "log", + "symphonia-core", +] + +[[package]] +name = "symphonia-utils-xiph" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" +dependencies = [ + "symphonia-core", + "symphonia-metadata", +] + +[[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.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thiserror" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +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.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.9", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.15", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +dependencies = [ + "futures-util", + "log", + "rustls 0.20.9", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", + "tungstenite 0.18.0", + "webpki", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tungstenite 0.21.0", + "webpki-roots 0.26.6", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[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.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "triomphe" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8f7726da4807b58ea5c96fdc122f80702030edc33b35aff9190a51148ccc85" + +[[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.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +dependencies = [ + "base64 0.13.1", + "byteorder", + "bytes", + "http 0.2.12", + "httparse", + "log", + "rand", + "rustls 0.20.9", + "sha1", + "thiserror", + "url", + "utf-8", + "webpki", +] + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.22.4", + "rustls-pki-types", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "twilight-gateway" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30be5c7e2b13b4a59e0f93344c070c23404279a318a324eece1f4384ead47d86" +dependencies = [ + "bitflags 1.3.2", + "futures-util", + "rand", + "rustls 0.20.9", + "rustls-native-certs", + "serde", + "serde_json", + "tokio", + "tokio-tungstenite 0.18.0", + "tracing", + "twilight-gateway-queue", + "twilight-model", +] + +[[package]] +name = "twilight-gateway-queue" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3073747da8e1d09bc5383eed750451c9534021c8206a20092405b9855b3cb35a" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "twilight-model" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276bd50f4817b3b421395afac89f5d7b61fdfd0f00a28b2a7db983e4878b4a1a" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde-value", + "serde_repr", + "time", +] + +[[package]] +name = "typemap_rev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "typesize" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dece5c06268af6a9ff4541788601e560a4284ffebfb357f713d676f13b964db" +dependencies = [ + "chrono", + "dashmap", + "hashbrown 0.14.5", + "mini-moka", + "parking_lot", + "secrecy", + "serde_json", + "time", + "typesize-derive", + "url", +] + +[[package]] +name = "typesize-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "unicase" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[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-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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 = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +dependencies = [ + "getrandom", +] + +[[package]] +name = "uwl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4bf03e0ca70d626ecc4ba6b0763b934b6f2976e8c744088bb3c1d646fbb1ad0" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[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.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[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.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.89", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[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-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[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" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[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-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", + "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_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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..caa2e6d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,43 @@ +[workspace] +members = [ + "crates/sonorust", + "crates/sonorust_db", + "crates/sonorust_logger", + "crates/engtokana", + "crates/setting_inputter", + "crates/sbv2_api", +] +resolver = "2" + +[workspace.dependencies] +sonorust_db.path = "crates/sonorust_db" +sonorust_logger.path = "crates/sonorust_logger" +engtokana.path = "crates/engtokana" +setting_inputter.path = "crates/setting_inputter" +sbv2_api.path = "crates/sbv2_api" +langrustang = { git = "https://github.com/aq2r/langrustang", tag = "v1.1.4" } +eq_uilibrium = { git = "https://github.com/aq2r/eq-uilibrium", tag = "v0.1.0" } + +anyhow = "1.0.91" +chrono = "0.4.38" +crossterm = "0.28.1" +dialoguer = "0.11.0" +env_logger = "0.11.5" +log = "0.4.22" +serde = { version = "1.0.215", features = ["derive"] } +serde_json = "1.0.128" +tokio = { version = "1.41.0", features = ["full"] } +rusqlite = { version = "0.32.1", features = ["bundled"] } +reqwest = { version = "0.12.7", features = ["json"] } +symphonia = { version = "0.5.4", features = ["aac", "mp3", "isomp4", "alac"] } +regex = "1.11.1" + +serenity = { version = "0.12.2", features = [ + "cache", + "client", + "standard_framework", + "rustls_backend", + "collector", + "utils", +] } +songbird = { version = "0.4.3", features = ["builtin-queue"] } diff --git a/LICENSE b/LICENSE index 0ad25db..ca9b055 100644 --- a/LICENSE +++ b/LICENSE @@ -617,45 +617,3 @@ Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/README-ja.md b/README-ja.md new file mode 100644 index 0000000..75cf006 --- /dev/null +++ b/README-ja.md @@ -0,0 +1,79 @@ +[English (Google or DeepL translate)](./README.md) | 日本語 + +# sonorust +Discord bot for Style-Bert-VITS2 + +SBV2の `server_fastapi.py` 用の Discord Botです。 + +## 機能 + +- ユーザーごとの `Model`, `Speaker`, `Style` の変更 + +- プレフィックスの変更 + +- アプリ起動時に SBV2 の API を自動起動 + +- サーバー辞書といくつかのサーバーオプション + +- 日本語と英語 (Google or DeepL translate) に対応 + +## 使用方法 + +ファイルを起動した後、表示に従って初期設定をします。 + +- デフォルト設定にするか + + - デフォルト設定でない場合、prefixやSBV2のURLなどを入力します。 + +- Botの言語設定 + +- SBV2の推論に使う言語設定 + +- 自動起動のためにSBV2のパスを設定するかどうか + +- Bot Tokenの入力 (Developer Portal から Intent すべてをONにしておく必要があります) + +## 基本的なコマンド + +prefix - コマンド名 でBotのコマンドを使用できます。また、スラッシュコマンドからも使用できます。 (デフォルトでは `sn!` ) + +### help + +Botのコマンド一覧を表示 + +### join + +使用者がいるボイスチャンネルに参加 + +### leave + +ボイスチャンネルから退席 + + +その他 10 個のコマンドは `help` コマンドから確認できます。 + + +
+ +# + +#### Lisense + + + + Copyright (C) 2024 aq2r + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + \ No newline at end of file diff --git a/README.md b/README.md index f37749c..b6d63a6 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,81 @@ +English (Google or DeepL translate) | [日本語](./README-ja.md) + # sonorust Discord bot for Style-Bert-VITS2 + +This is a Discord bot for SBV2's `server_fastapi.py`. + +## App Features + +- Change `Model`, `Speaker`, `Style` for each user + +- Prefix Changes + +- Automatically launch SBV2 API when the app starts + +- Server dictionary and some server options + +- Supports Japanese and English (Google or DeepL translate) + +## 使用方法 + +After launching the file, follow the on-screen instructions to complete the initial setup. + +- Whether to make it the default setting + + - If you do not want to use the default settings, enter the prefix, SBV2 URL, etc. + +- Bot language settings + +- Language settings to use for SBV2 inference + +- Whether to set the SBV2 path for automatic startup + +- Enter the Bot Token (All Intents must be turned ON in the Developer Portal) + +## Basic Commands + +`prefix - The command name` can be used to execute bot commands. + +It can also be used from slash commands. (By default, `sn!`) + +### help + +Display the list of bot commands + +### join + +Join a voice channel that the user is in + +### leave + +Leave a voice channel + + +The other 10 commands can be seen using the `help` command. + + +
+ +# + +#### Lisense + + + + Copyright (C) 2024 aq2r + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + + \ No newline at end of file diff --git a/about.hbs b/about.hbs new file mode 100644 index 0000000..699b3b0 --- /dev/null +++ b/about.hbs @@ -0,0 +1,70 @@ + + + + + + + +
+
+

Third Party Licenses

+

This page lists the licenses of the projects used in cargo-about.

+
+ +

Overview of licenses:

+
    + {{#each overview}} +
  • {{name}} ({{count}})
  • + {{/each}} +
+ +

All license text:

+ +
+ + + diff --git a/about.toml b/about.toml new file mode 100644 index 0000000..6ad2ab9 --- /dev/null +++ b/about.toml @@ -0,0 +1,15 @@ +accepted = [ + "Apache-2.0", + "MIT", + "BSD-3-Clause", + "Zlib", + "MPL-2.0", + "Unicode-DFS-2016", + "ISC", + "OpenSSL", + "OpenSSL-standalone", + "SSLeay-standalone", + "GPL-1.0", + "LicenseRef-scancode-public-domain", + "LicenseRef-scancode-unknown-license-reference", +] diff --git a/crates/engtokana/Cargo.toml b/crates/engtokana/Cargo.toml new file mode 100644 index 0000000..f3f4aa6 --- /dev/null +++ b/crates/engtokana/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "engtokana" +edition = "2021" +publish = false + +[dependencies] +tokio.workspace = true +reqwest.workspace = true +anyhow.workspace = true +log.workspace = true diff --git a/crates/engtokana/src/lib.rs b/crates/engtokana/src/lib.rs new file mode 100644 index 0000000..616da75 --- /dev/null +++ b/crates/engtokana/src/lib.rs @@ -0,0 +1,266 @@ +use std::{ + collections::HashMap, + path::PathBuf, + sync::{LazyLock, RwLock, RwLockReadGuard}, +}; + +use tokio::{fs::File, io::AsyncReadExt}; + +// 辞書のパスを設定する (下のURLの bep-eng.dic を入れる) +// https://fastapi.metacpan.org/source/MASH/Lingua-JA-Yomi-0.01/lib/Lingua/JA +const BEPENG_DIC_URL: &str = + "https://fastapi.metacpan.org/source/MASH/Lingua-JA-Yomi-0.01/lib/Lingua/JA/bep-eng.dic"; +pub const BEPENG_DIC_PATH: &str = "./appdata/downloads/bep-eng.dic"; +pub const BEPENG_DIC_FOLDER: &str = "./appdata/downloads"; + +static TRANS_DICT: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +pub struct EngToKana<'a> { + dict_data: RwLockReadGuard<'a, HashMap>, +} + +impl EngToKana<'_> { + pub async fn download_init_dic() -> anyhow::Result<()> { + /* 辞書のダウンロード */ + + // すでにダウンロードしてある場合ダウンロードしない + let dic_path = PathBuf::from(BEPENG_DIC_PATH); + if !dic_path.exists() { + log::info!("Downloading kana reading dictionary..."); + + // ダウンロード + let response = reqwest::get(BEPENG_DIC_URL).await?; + let bytes = response.bytes().await?; + + // フォルダ作成と書き込み + tokio::fs::create_dir_all(BEPENG_DIC_FOLDER).await?; + + let mut file = File::create(BEPENG_DIC_PATH).await?; + tokio::io::copy(&mut bytes.as_ref(), &mut file).await?; + + log::info!("Kana reading dictionary download is complete."); + } + + /* static TRANS_DICT の初期化 */ + + let mut bepeng_dic = File::open(BEPENG_DIC_PATH).await?; + let mut dic_text = String::new(); + + bepeng_dic.read_to_string(&mut dic_text).await?; + + // 改行で区切って、空白行、コメント行を消す + let vec: Vec<&str> = dic_text + .split("\n") + .filter(|s| *s != "") + .filter(|s| !s.starts_with("#")) + .collect(); + + let mut trans_dict: HashMap = HashMap::with_capacity(vec.len()); + + // それぞれの要素を空白で前半と後半で分割して、trans_dict に組み合わせを登録 + vec.iter().for_each(|s| { + let split: Vec<&str> = s.split(" ").collect(); + let before = split[0].to_string(); + let after = split[1].to_string(); + + trans_dict.insert(before, after); + }); + + // static 変数を更新 + { + let mut lock = TRANS_DICT.write().unwrap(); + *lock = trans_dict + } + + Ok(()) + } + + /// 入力された文章の英語をすべてカタカナ読みに変換する。 + /// + /// もし "VeryVeryExcellent" のように英単語がつながっていても、 + /// + /// 単語ごとに分割して、"ベリーベリーエクセレント" のように変換する。 + pub fn convert_all(target_text: &str) -> String { + let lock = TRANS_DICT.read().unwrap(); + let engtokana = Self { dict_data: lock }; + + let mut result_words = vec![]; + + for i in engtokana.split_en_other(target_text) { + for splited in engtokana.split_word(i) { + result_words.push(engtokana.convert_single_word(splited)); + } + } + + let s = result_words.join(""); + return s; + } + + /// 英語とそれ以外の単語で分割する + /// + /// あいうabcえお -> [あいう, abc, えお] + fn split_en_other<'a>(&self, target_text: &'a str) -> Vec<&'a str> { + let mut result_text = vec![]; + + // 日本語と英語でそれぞれ分割して、その最初と最後の番目を取得する + let separate_index: Vec<[usize; 2]> = { + let mut boundaries = Vec::new(); + let mut chars = target_text.char_indices(); + + if let Some((start, c)) = chars.next() { + let mut is_ascii_alphabetic = c.is_ascii_alphabetic(); + let mut range_start = start; + + for (i, c) in chars { + if (c.is_ascii_alphabetic()) != is_ascii_alphabetic { + boundaries.push([range_start, i]); + range_start = i; + is_ascii_alphabetic = c.is_ascii_alphabetic(); + } + } + boundaries.push([range_start, target_text.len()]); + } + + boundaries + }; + + // 取得した位置からstrを取得して result_text に入れる + for i in separate_index { + result_text.push(&target_text[i[0]..i[1]]); + } + + return result_text; + } + + /// 単語単位で英語をカタカナ読みに変換する + /// + /// 変換できないものはそのまま返す + pub fn convert_single_word<'a>(&'a self, word: &'a str) -> &'a str { + if !self.is_convert_target(word) { + return word; + } + + let to_kana_result = self.get_kana_from_dict(word); + + // 変換可能だったら変換したもの、できなかったら入力をそのまま返す + if let Some(kana) = to_kana_result { + return kana; + } else { + return word; + } + } + + /// TRANS_DICT から対応する値を取り出す + fn get_kana_from_dict(&self, key: &str) -> Option<&str> { + if let Some(kana) = self.dict_data.get(&key.to_uppercase()) { + Some(kana.as_str()) + } else { + None + } + } + + /// 渡された単語が英語かつ、大文字だけではないなら True を返す + fn is_convert_target(&self, word: &str) -> bool { + // 英語かどうか + if word.is_ascii() { + //大文字だけかどうか + if word.chars().all(|c| c.is_uppercase()) { + false + } else { + true + } + } else { + false + } + } + + /// つながった英単語を分割して Vec にして返す + /// + /// 分割の必要がない場合やできなかった場合は入力をそのまま Vec に入れて返す + /// + /// 英語ではない場合も、Vec に入れてそのまま返す + fn split_word<'a>(&self, target_str: &'a str) -> Vec<&'a str> { + if !self.is_convert_target(target_str) { + return vec![&target_str]; // 変換したい文字ではない場合、そのまま返す + } + + let target_str_upper = &target_str.to_uppercase(); + if self.dict_data.contains_key(target_str_upper) { + return vec![&target_str]; // target_word が辞書にある場合そのまま返す + } + + // target_word の1文字目から、文字数-1文字目, 文字数-2文字目, ... と見ていく + for i in (0..target_str.chars().count()).rev() { + let target_prefix = &target_str_upper[..i]; + + // target_word の最初の方が辞書にあったら + if self.dict_data.contains_key(target_prefix) { + let target_suffix = &target_str[i..]; + + // target_word から見つかった単語を引いて、まだ文字が残っているなら + if !target_suffix.is_empty() { + let target_suffix_split = self.split_word(target_suffix); // さらに残りを再帰で分割する + + let mut vec = vec![&target_str[..i]]; + vec.extend(target_suffix_split); + return vec; + } else { + return vec![&target_str[..i]]; + } + } + } + + return vec![&target_str]; // 分割できなかったら入力をそのまま返す + } +} + +#[cfg(test)] +mod tests { + use std::time::Instant; + + use crate::EngToKana; + + #[tokio::test] + async fn test_download_dic() -> anyhow::Result<()> { + EngToKana::download_init_dic().await?; + EngToKana::download_init_dic().await?; + + Ok(()) + } + + #[tokio::test] + async fn convert_all() -> anyhow::Result<()> { + EngToKana::download_init_dic().await?; + + let now = Instant::now(); + let result = EngToKana::convert_all("Hello"); + dbg!(now.elapsed()); + assert_eq!("ハロー", result); + + let now = Instant::now(); + let result = EngToKana::convert_all("Hello"); + dbg!(now.elapsed()); + assert_eq!("ハロー", result); + + let now = Instant::now(); + let result = EngToKana::convert_all("こんにちはworld!"); + dbg!(now.elapsed()); + assert_eq!("こんにちはワールドゥ!".to_string(), result); + + let now = Instant::now(); + let result = EngToKana::convert_all("veryveryexcellent"); + dbg!(now.elapsed()); + assert_eq!("ベリーベリーエクセレントゥ", result); + + let now = Instant::now(); + let result = EngToKana::convert_all("veryveryexcellentveryveryexcellentveryveryexcellent"); + dbg!(now.elapsed()); + assert_eq!( + "ベリーベリーエクセレントゥベリーベリーエクセレントゥベリーベリーエクセレントゥ", + result + ); + + Ok(()) + } +} diff --git a/crates/sbv2_api/Cargo.toml b/crates/sbv2_api/Cargo.toml new file mode 100644 index 0000000..e403a0f --- /dev/null +++ b/crates/sbv2_api/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sbv2_api" +edition = "2021" + +[dependencies] +reqwest.workspace = true +tokio.workspace = true +anyhow.workspace = true +serde_json.workspace = true diff --git a/crates/sbv2_api/src/client.rs b/crates/sbv2_api/src/client.rs new file mode 100644 index 0000000..1be6b6c --- /dev/null +++ b/crates/sbv2_api/src/client.rs @@ -0,0 +1,173 @@ +use std::{path::PathBuf, process, time::Duration}; + +use anyhow::Context as _; + +use crate::model_info::Sbv2ModelInfo; + +pub struct Sbv2Client { + pub host: String, + pub port: u32, + + pub(crate) client: reqwest::Client, +} + +pub struct Sbv2InferParam { + pub model_id: u32, + pub speaker_id: u32, + pub style_name: String, + pub length: f64, + pub language: String, +} + +impl Sbv2Client { + pub fn from(host: &str, port: u32) -> Self { + let client = reqwest::Client::new(); + + Self { + host: host.into(), + port, + client, + } + } + + pub async fn update_modelinfo(&self) -> anyhow::Result<()> { + Sbv2ModelInfo::update_modelinfo(self).await?; + Ok(()) + } + + pub async fn is_api_activation(&self) -> bool { + let url = { + let host = self.host.as_str(); + let port = self.port; + + format!("http://{host}:{port}/models/refresh") + }; + + match self.client.get(url).send().await { + Ok(_) => true, + Err(_) => false, + } + } + + pub async fn infer(&self, text: &str, param: Sbv2InferParam) -> anyhow::Result> { + let url = { + // パラメーター設定 + let host = self.host.as_str(); + let port = self.port; + let model_id = param.model_id; + let speaker_id = param.speaker_id; + let style_name = param.style_name; + let length = param.length; + + let sdp_ratio = 0.2; + let noise = 0.6; + let noisew = 0.8; + + let language = match param.language.as_str() { + "Jp" => "JP", + "Ja" => "JP", + "En" => "EN", + "Zh" => "ZH", + + "JP" => "JP", + "JA" => "JP", + "EN" => "EN", + "ZH" => "ZH", + + "jp" => "JP", + "ja" => "JP", + "en" => "EN", + "zh" => "ZH", + + _ => "JP", + }; + + format!( + "\ + http://{host}:{port}/voice?\ + text={text}&\ + encoding=utf-8&\ + model_id={model_id}&\ + speaker_id={speaker_id}&\ + sdp_ratio={sdp_ratio}&\ + noise={noise}&\ + noisew={noisew}&\ + length={length}&\ + language={language}&\ + auto_split=true&\ + split_interval=0.5&\ + assist_text_weight=1&\ + style={style_name}&\ + style_weight=5" + ) + }; + + let res = self.client.get(url).send().await?; + + let bytes = res.bytes().await?; + + Ok(bytes.to_vec()) + } + + pub async fn launch_api_win(&self, sbv2_path: &str) -> anyhow::Result<()> { + let sbv2_path = PathBuf::from(sbv2_path); + let python_path = sbv2_path.join("venv/Scripts/python.exe"); + let api_py_path = sbv2_path.join("server_fastapi.py"); + + let mut child = process::Command::new("cmd") + .args([ + "/C", + "start", + python_path.to_str().context("launch_api Failed")?, + api_py_path.to_str().context("launch_api Failed")?, + ]) + .current_dir(sbv2_path) + .spawn()?; + + child.wait()?; + + loop { + match self.is_api_activation().await { + true => break, + false => tokio::time::sleep(Duration::from_secs(3)).await, + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::sync::LazyLock; + + use crate::Sbv2InferParam; + + use super::Sbv2Client; + + static CLIENT: LazyLock = LazyLock::new(|| Sbv2Client::from("127.0.0.1", 5000)); + + #[tokio::test] + async fn test_is_api_activation() { + dbg!(CLIENT.is_api_activation().await); + } + + #[ignore] + #[tokio::test] + async fn test_infer() { + let _ = dbg!( + CLIENT + .infer( + "こんにちは", + Sbv2InferParam { + model_id: 0, + speaker_id: 0, + style_name: "Default".into(), + length: 1.0, + language: "JP".into() + } + ) + .await + ); + } +} diff --git a/crates/sbv2_api/src/lib.rs b/crates/sbv2_api/src/lib.rs new file mode 100644 index 0000000..163ed44 --- /dev/null +++ b/crates/sbv2_api/src/lib.rs @@ -0,0 +1,5 @@ +mod client; +mod model_info; + +pub use client::{Sbv2Client, Sbv2InferParam}; +pub use model_info::{ModelInfo, Sbv2ModelInfo, ValidModel, SBV2_MODELINFO}; diff --git a/crates/sbv2_api/src/model_info.rs b/crates/sbv2_api/src/model_info.rs new file mode 100644 index 0000000..3cd37b3 --- /dev/null +++ b/crates/sbv2_api/src/model_info.rs @@ -0,0 +1,182 @@ +use std::{ + collections::HashMap, + sync::{LazyLock, RwLock}, +}; + +use anyhow::Context as _; + +use crate::client::Sbv2Client; + +pub static SBV2_MODELINFO: LazyLock> = LazyLock::new(|| { + RwLock::new(Sbv2ModelInfo { + name_to_model: HashMap::new(), + id_to_model: HashMap::new(), + }) +}); + +/// 特定のモデルの情報を入れておく構造体 +#[derive(Debug, Clone)] +pub struct ModelInfo { + pub model_id: u32, + pub model_name: String, + pub spk2id: HashMap, + pub id2spk: HashMap, + pub style2id: HashMap, + pub id2style: HashMap, +} + +/// APIの全てのモデルの情報を入れておく構造体 +#[derive(Debug)] +pub struct Sbv2ModelInfo { + pub name_to_model: HashMap, + pub id_to_model: HashMap, +} + +/// Sbv2ModelInfoに含まれるモデル +#[derive(Debug)] +pub struct ValidModel { + pub model_name: String, + pub speaker_name: String, + pub style_name: String, + pub model_id: u32, + pub speaker_id: u32, +} + +impl Sbv2ModelInfo { + pub(crate) async fn update_modelinfo(client: &Sbv2Client) -> anyhow::Result<()> { + let url = { + let host = client.host.as_str(); + let port = client.port; + + format!("http://{host}:{port}/models/refresh") + }; + + let client = &client.client; + + let modelinfo_text = client.post(url).send().await?.text().await?; + let json_value: serde_json::Value = serde_json::from_str(&modelinfo_text)?; + + let mut name_to_model = HashMap::new(); + let mut id_to_model = HashMap::new(); + + for (model_id_obj, model_info_obj) in json_value.as_object().unwrap().iter() { + let model_id: u32 = model_id_obj.parse()?; + + // モデルのフォルダ名 + let folder_name = model_info_obj["config_path"] + .to_string() + .split("\\\\") + .nth(1) + .context("None Error")? + .to_string(); + + // HashMap + let spk2id: HashMap<_, _> = model_info_obj["spk2id"] + .as_object() + .context("None Error")? + .iter() + .map(|(speaker_name, speaker_id)| { + ( + speaker_name.to_string(), + speaker_id.as_u64().unwrap() as u32, + ) + }) + .collect(); + + // HashMap + let style2id: HashMap<_, _> = model_info_obj["style2id"] + .as_object() + .context("None Error")? + .iter() + .map(|(style_name, style_id)| { + (style_name.to_string(), style_id.as_u64().unwrap() as u32) + }) + .collect(); + + // キーと値を反転 + let id2spk: HashMap<_, _> = spk2id.iter().map(|(k, v)| (*v, k.clone())).collect(); + let id2style: HashMap<_, _> = style2id.iter().map(|(k, v)| (*v, k.clone())).collect(); + + let model_info = ModelInfo { + model_id, + model_name: folder_name.clone(), + spk2id, + id2spk, + style2id, + id2style, + }; + + name_to_model.insert(folder_name, model_info.clone()); + id_to_model.insert(model_id, model_info); + } + + let sbv2_modelinfo = Self { + name_to_model, + id_to_model, + }; + + { + let mut lock = SBV2_MODELINFO.write().unwrap(); + *lock = sbv2_modelinfo; + } + + Ok(()) + } + + /// 引数をもとにSbv2ModelInfoに含まれる有効なモデルを取得する + pub fn get_valid_model( + model_name: &str, + speaker_name: &str, + style_name: &str, + default_model: &str, + ) -> ValidModel { + let sbv2_modelinfo = SBV2_MODELINFO.read().unwrap(); + + // デフォルトモデルが無ければ デフォルトモデル、さらに無ければ id が 0 のものを返す + let model = match sbv2_modelinfo.name_to_model.get(model_name) { + Some(model) => model, + + None => match sbv2_modelinfo.name_to_model.get(default_model) { + Some(model) => model, + None => sbv2_modelinfo.id_to_model.get(&0).unwrap(), + }, + }; + let valid_model_name = model.model_name.clone(); + let valid_model_id = model.model_id; + + // 指定した話者が存在しなければ id が 0 の話者を選択する + let valid_speaker_id = match model.spk2id.get(speaker_name) { + Some(id) => *id, + None => 0, + }; + let valid_speaker_name = model.id2spk.get(&valid_speaker_id).unwrap().clone(); + + // 指定したスタイルが存在しなければ id が 0 のスタイルを選択する + let valid_style_id = match model.style2id.get(style_name) { + Some(id) => *id, + None => 0, + }; + let valid_style_name = model.id2style.get(&valid_style_id).unwrap().clone(); + + ValidModel { + model_name: valid_model_name, + speaker_name: valid_speaker_name, + style_name: valid_style_name, + model_id: valid_model_id, + speaker_id: valid_speaker_id, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn update_modelinfo() { + let client = Sbv2Client::from("127.0.0.1", 5000); + + Sbv2ModelInfo::update_modelinfo(&client).await.unwrap(); + println!("{:#?}", *(SBV2_MODELINFO.read().unwrap())) + } +} diff --git a/crates/setting_inputter/Cargo.toml b/crates/setting_inputter/Cargo.toml new file mode 100644 index 0000000..a661e4c --- /dev/null +++ b/crates/setting_inputter/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "setting_inputter" +edition = "2021" +publish = false + +[dependencies] +dialoguer.workspace = true +serde.workspace = true +serde_json.workspace = true +log.workspace = true +crossterm.workspace = true +tokio.workspace = true +anyhow.workspace = true diff --git a/crates/setting_inputter/src/lib.rs b/crates/setting_inputter/src/lib.rs new file mode 100644 index 0000000..0c6fce8 --- /dev/null +++ b/crates/setting_inputter/src/lib.rs @@ -0,0 +1,5 @@ +pub mod settings_json; +pub mod token; + +pub use settings_json::SettingsJson; +pub use token::{get_or_set_token, input_token}; diff --git a/crates/setting_inputter/src/settings_json.rs b/crates/setting_inputter/src/settings_json.rs new file mode 100644 index 0000000..9396836 --- /dev/null +++ b/crates/setting_inputter/src/settings_json.rs @@ -0,0 +1,230 @@ +use std::{ + fmt::Display, + fs::{self, create_dir_all, File}, + io::Write, + path::PathBuf, + sync::{LazyLock, RwLock}, +}; + +use dialoguer::{Confirm, Input, Select}; +use serde::{Deserialize, Serialize}; + +const APPDATA_PATH: &str = "./appdata"; +const JSON_PATH: &str = "./appdata/settings.json"; +pub static SETTINGS_JSON: LazyLock> = LazyLock::new(|| { + RwLock::new(SettingsJson { + sbv2_path: None, + read_limit: 50, + default_model: "".into(), + prefix: "sn!".into(), + host: "127.0.0.1".into(), + port: 5000, + bot_lang: SettingLang::Ja, + infer_lang: SettingLang::Ja, + }) +}); + +#[derive(Serialize, Deserialize, Debug, Clone, Copy)] +pub enum SettingLang { + Ja, + En, +} + +impl Display for SettingLang { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + SettingLang::Ja => "Ja", + SettingLang::En => "En", + }; + + write!(f, "{}", s) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct SettingsJson { + pub sbv2_path: Option, + pub read_limit: u32, + pub default_model: String, + pub prefix: String, + pub host: String, + pub port: u32, + pub bot_lang: SettingLang, + pub infer_lang: SettingLang, +} + +impl SettingsJson { + pub fn new() -> SettingsJson { + let text = match fs::read_to_string(JSON_PATH) { + Ok(text) => text, + Err(_) => Self::init_json(), + }; + + match serde_json::from_str::(&text) { + Ok(json) => json, + Err(_) => { + Self::init_json(); + Self::new() + } + } + } + + /// jsonファイルが存在しなければユーザーに聞いて作成する + fn init_json() -> String { + log::info!("Perform initial settings."); + + let is_default = Confirm::new() + .with_prompt("Do you want to use the default settings?") + .default(true) + .interact() + .unwrap(); + + let string = match is_default { + true => default_process(), + false => not_default_process(), + }; + + create_dir_all(APPDATA_PATH).expect("Can't open file."); + let mut file = File::create(JSON_PATH).expect("Can't open file."); + file.write_all(string.as_bytes()).expect("Can't open file."); + + string + } + + /// jsonファイルへの書き込みと LazyLock の更新を行う + pub fn dump_json(settings_json: SettingsJson) { + let json_str = serde_json::to_string_pretty(&settings_json).unwrap(); + + create_dir_all(APPDATA_PATH).expect("Can't open file."); + let mut file = File::create(JSON_PATH).expect("Can't open file."); + file.write_all(json_str.as_bytes()) + .expect("Can't open file."); + + { + let mut lock = SETTINGS_JSON.write().unwrap(); + *lock = settings_json; + } + } +} + +/// 言語選択 +fn select_lang() -> (SettingLang, SettingLang) { + let choices = ["En (Google or DeepL translate)", "Ja"]; + println!("Select Bot language:"); + let selection_bot = Select::new().items(&choices).interact().unwrap(); + + let choices = ["En", "Ja", "Zh"]; + println!("Select SBV2 Infer language:"); + let selection_infer = Select::new().items(&choices).interact().unwrap(); + + ( + match selection_bot { + 0_usize => SettingLang::En, + 1_usize => SettingLang::Ja, + _ => SettingLang::Ja, + }, + match selection_infer { + 0_usize => SettingLang::En, + 1_usize => SettingLang::Ja, + _ => SettingLang::Ja, + }, + ) +} + +/// sbv2のパスを入力してもらう +fn input_sbv2_path() -> Option { + let if_input_path = Confirm::new() + .with_prompt("Do you want to set the path for SBV2 to start automatically?") + .default(true) + .interact() + .unwrap(); + + if !if_input_path { + return None; + } + + // sbv2 のパスを入力してもらう + let input: String = dialoguer::Input::new() + .with_prompt("Please enter the SBV2 path") + .validate_with(|input: &String| -> Result<(), &str> { + let input_path = PathBuf::from(input); + let server_fastapi_py = input_path.join("server_fastapi.py"); + let python_exe = input_path.join("venv/Scripts/python.exe"); + + match (server_fastapi_py.exists(), python_exe.exists()) { + (true, true) => Ok(()), + _ => Err("The path you entered is not an SBV2 path."), + } + }) + .interact() + .unwrap(); + + Some(input) +} + +/// ユーザーがデフォルト設定を使用するを選択したときの処理 +fn default_process() -> String { + let (bot_lang, infer_lang) = select_lang(); + let sbv2_path = input_sbv2_path(); + + let settings_json = SettingsJson { + sbv2_path, + read_limit: 50, + default_model: "".to_string(), + prefix: "sn!".to_string(), + host: "127.0.0.1".to_string(), + port: 5000, + bot_lang, + infer_lang, + }; + + serde_json::to_string_pretty(&settings_json).unwrap() +} + +/// ユーザーがデフォルト設定を使用しないを選択したときの処理 +fn not_default_process() -> String { + let read_limit: u32 = Input::new() + .with_prompt("Input `Maximum number of characters to read`") + .with_initial_text("50") + .interact_text() + .unwrap(); + + let default_model: String = Input::new() + .with_prompt("Input `Default model name`") + .with_initial_text("None") + .interact_text() + .unwrap(); + + let prefix: String = Input::new() + .with_prompt("Input `Prefix`") + .with_initial_text("sn!") + .interact_text() + .unwrap(); + + let host: String = Input::new() + .with_prompt("Input `SBV2 API host`") + .with_initial_text("127.0.0.1") + .interact_text() + .unwrap(); + + let port: u32 = Input::new() + .with_prompt("Input `SBV2 API port`") + .with_initial_text("5000") + .interact_text() + .unwrap(); + + let (bot_lang, infer_lang) = select_lang(); + let sbv2_path = input_sbv2_path(); + + let settings_json = SettingsJson { + sbv2_path, + read_limit, + default_model, + prefix, + host, + port, + bot_lang, + infer_lang, + }; + serde_json::to_string_pretty(&settings_json).unwrap() +} diff --git a/crates/setting_inputter/src/token.rs b/crates/setting_inputter/src/token.rs new file mode 100644 index 0000000..90a2899 --- /dev/null +++ b/crates/setting_inputter/src/token.rs @@ -0,0 +1,49 @@ +use std::{ + io::{self, stdin, stdout, Write}, + path::PathBuf, +}; + +use crossterm::{ + cursor::{MoveToColumn, MoveUp}, + execute, + terminal::{Clear, ClearType}, +}; +use tokio::{ + fs::{self, create_dir_all, File}, + io::AsyncWriteExt, +}; + +const TOKEN_FILEPATH: &str = "./appdata/BOT_TOKEN"; + +pub async fn get_or_set_token() -> anyhow::Result { + match fs::read_to_string(TOKEN_FILEPATH).await { + Ok(string) => Ok(string), + Err(_) => input_token().await, + } +} + +pub async fn input_token() -> anyhow::Result { + log::info!("Input Bot Token"); + print!("Your Bot Token: "); + stdout().flush()?; + + let mut buffer = String::new(); + stdin().read_line(&mut buffer)?; + + let input = buffer.trim().to_string(); + + create_dir_all(PathBuf::from(TOKEN_FILEPATH).ancestors().nth(1).unwrap()).await?; + let mut file = File::create(TOKEN_FILEPATH).await?; + file.write_all(input.as_bytes()).await?; + + // 入力内容を書き換え + execute!( + io::stdout(), + MoveUp(1), + Clear(ClearType::FromCursorDown), + MoveToColumn(0) + )?; + println!("Your Bot Token: {}", "*".repeat(input.len())); + + Ok(input) +} diff --git a/crates/sonorust/Cargo.toml b/crates/sonorust/Cargo.toml new file mode 100644 index 0000000..d166dd2 --- /dev/null +++ b/crates/sonorust/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "sonorust" +edition = "2021" +publish = false + +[dependencies] +sonorust_db.workspace = true +sonorust_logger.workspace = true +engtokana.workspace = true +setting_inputter.workspace = true +sbv2_api.workspace = true +langrustang.workspace = true +eq_uilibrium.workspace = true + +serenity.workspace = true +symphonia.workspace = true +tokio.workspace = true +log.workspace = true +songbird.workspace = true +anyhow.workspace = true +regex.workspace = true diff --git a/crates/sonorust/src/commands/dict.rs b/crates/sonorust/src/commands/dict.rs new file mode 100644 index 0000000..c083ba4 --- /dev/null +++ b/crates/sonorust/src/commands/dict.rs @@ -0,0 +1,72 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ + ButtonStyle, Context, CreateActionRow, CreateButton, CreateCommand, CreateEmbed, GuildId, +}; +use setting_inputter::SettingsJson; +use sonorust_db::GuildData; + +use crate::{ + crate_extensions::SettingsJsonExtension, + errors::{NoneToSonorustError, SonorustError}, +}; + +pub async fn dict( + ctx: &Context, + guild_id: Option, +) -> Result<(CreateEmbed, Vec), SonorustError> { + let guild_id = guild_id.ok_or_sonorust_err()?; + let guilddata = GuildData::from(guild_id).await?; + + let guild_name = guild_id + .name(&ctx.cache) + .unwrap_or_else(|| "Unknown".to_string()); + + // コンポーネントと embed の作成 + let component_row0 = create_button_row(); + let embed = create_embed(&guild_name, guilddata); + + Ok((embed, vec![component_row0])) +} + +fn create_embed(guild_name: &str, guilddata: GuildData) -> CreateEmbed { + let lang = SettingsJson::get_bot_lang(); + + let server_dict = guilddata.dict; + let mut description = String::default(); + + // embed の内容作成 + for (i, (k, v)) in server_dict.iter().enumerate() { + match i { + 0 => description += &format!("{k} -> {v}"), + _ => description += &format!("\n{k} -> {v}"), + } + } + + if server_dict.len() == 0 { + description = lang_t!("dict.unregistered", lang).to_string(); + } + + if description.len() >= 4000 { + description = lang_t!("dict.too_many", lang).to_string() + } + + let title = format_t!("dict.embed.title", lang, guild_name); + CreateEmbed::new().title(title).description(description) +} + +fn create_button_row() -> CreateActionRow { + let button_add = CreateButton::new(lang_t!("customid.dict.add")) + .label(lang_t!("dict.label.add")) + .style(ButtonStyle::Primary); + let button_remove = CreateButton::new(lang_t!("customid.dict.remove")) + .label(lang_t!("dict.label.remove")) + .style(ButtonStyle::Secondary); + + CreateActionRow::Buttons(vec![button_add, button_remove]) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("dict").description(lang_t!("dict.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/help.rs b/crates/sonorust/src/commands/help.rs new file mode 100644 index 0000000..d0d682b --- /dev/null +++ b/crates/sonorust/src/commands/help.rs @@ -0,0 +1,103 @@ +use langrustang::lang_t; +use serenity::all::{Context, CreateCommand, CreateEmbed}; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; + +use crate::crate_extensions::SettingsJsonExtension; + +pub async fn help(ctx: &Context) -> CreateEmbed { + let lang = SettingsJson::get_bot_lang(); + const IS_INLINE: bool = false; + + let prefix = { + let lock = SETTINGS_JSON.read().unwrap(); + lock.prefix.clone() + }; + + // TODO: コマンドヘルプの追加 + let fields = [ + ( + lang_t!("ping.command.name"), + lang_t!("ping.command.description"), + IS_INLINE, + ), + ( + lang_t!("help.command.name"), + lang_t!("help.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("now.command.name"), + lang_t!("now.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("join.command.name"), + lang_t!("join.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("leave.command.name"), + lang_t!("leave.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("model.command.name"), + lang_t!("model.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("speaker.command.name"), + lang_t!("speaker.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("style.command.name"), + lang_t!("style.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("server.command.name"), + lang_t!("server.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("dict.command.name"), + lang_t!("dict.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("reload.command.name"), + lang_t!("reload.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("length.command.name"), + lang_t!("length.command.description", lang), + IS_INLINE, + ), + ( + lang_t!("wav.command.name"), + lang_t!("wav.command.description", lang), + IS_INLINE, + ), + ]; + + let bot_user = ctx.cache.current_user(); + let avatar_url = bot_user + .avatar_url() + .unwrap_or_else(|| bot_user.default_avatar_url()); + + let fields = fields + .map(|(name, description, is_inline)| (format!("{prefix}{name}"), description, is_inline)); + + CreateEmbed::new() + .title(lang_t!("help.embed.title", lang)) + .fields(fields) + .thumbnail(avatar_url) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("help").description(lang_t!("help.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/join.rs b/crates/sonorust/src/commands/join.rs new file mode 100644 index 0000000..f406951 --- /dev/null +++ b/crates/sonorust/src/commands/join.rs @@ -0,0 +1,79 @@ +use std::collections::VecDeque; + +use langrustang::lang_t; +use serenity::all::{ChannelId, Context, CreateCommand, GuildId, UserId}; +use setting_inputter::SettingsJson; + +use crate::crate_extensions::{ + sbv2_api::{CHANNEL_QUEUES, READ_CHANNELS}, + SettingsJsonExtension, +}; + +pub async fn join( + ctx: &Context, + guild_id: Option, + channel_id: ChannelId, + user_id: UserId, +) -> Result<&'static str, &'static str> { + let lang = SettingsJson::get_bot_lang(); + + // guild_id を取得、DM などの場合返す + let Some(guild_id) = guild_id else { + return Err(lang_t!("msg.only_use_guild_2", lang)); + }; + + // もしそのサーバーの VC にすでにいる場合返す + let manager = songbird::get(ctx).await.unwrap(); + if let Some(_) = manager.get(guild_id) { + return Err(lang_t!("join.already", lang)); + } + + // ユーザーがいる VC を取得する + let user_vc = { + let Some(guild) = guild_id.to_guild_cached(&ctx.cache) else { + log::error!(lang_t!("log.fail_get_guild")); + return Err(lang_t!("join.cannot_connect", lang)); + }; + + guild.voice_states.get(&user_id).and_then(|v| v.channel_id) + }; + + // 使用したユーザーが VC に参加していない場合返す + let Some(connect_ch) = user_vc else { + return Err(lang_t!("join.after_connecting", lang)); + }; + + // もし VC に参加できなかったら返す + if let Err(err) = manager.join(guild_id, connect_ch).await { + log::error!("{}: {err}", lang_t!("log.fail_join_vc")); + return Err(lang_t!("join.cannot_connect", lang)); + } else { + log::debug!( + "Joined voice channel (name: {} id: {})", + guild_id + .name(&ctx.cache) + .unwrap_or_else(|| "Unknown".to_string()), + guild_id, + ); + } + + // サーバーIDと読み上げるチャンネルIDのペアを登録 + { + let mut read_channels = READ_CHANNELS.write().unwrap(); + read_channels.insert(guild_id, channel_id); + } + + // 読み上げ queue を初期化 + { + let mut channel_queues = CHANNEL_QUEUES.write().unwrap(); + channel_queues.insert(channel_id, VecDeque::new()); + } + + Ok(lang_t!("join.connected", lang)) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("join").description(lang_t!("join.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/leave.rs b/crates/sonorust/src/commands/leave.rs new file mode 100644 index 0000000..e63a00a --- /dev/null +++ b/crates/sonorust/src/commands/leave.rs @@ -0,0 +1,40 @@ +use langrustang::lang_t; +use serenity::all::{Context, CreateCommand, GuildId}; +use setting_inputter::SettingsJson; + +use crate::crate_extensions::{sbv2_api::READ_CHANNELS, SettingsJsonExtension}; + +pub async fn leave(ctx: &Context, guild_id: Option) -> &'static str { + let lang = SettingsJson::get_bot_lang(); + + let Some(guild_id) = guild_id else { + return lang_t!("msg.only_use_guild_2", lang); + }; + + let manager = songbird::get(ctx).await.unwrap(); + + // ボットがvcにいるなら切断、いないなら接続していませんと返す + match manager.get(guild_id) { + Some(_) => { + if let Err(err) = manager.remove(guild_id).await { + log::error!("{}: {err}", lang_t!("log.fail_leave_vc")); + return lang_t!("leave.cannot_disconnect", lang); + } + } + None => return lang_t!("leave.already", lang), + } + + // 読み上げる対象から外す + { + let mut read_channels = READ_CHANNELS.write().unwrap(); + read_channels.remove(&guild_id); + } + + lang_t!("leave.disconnected", lang) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("leave").description(lang_t!("leave.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/length.rs b/crates/sonorust/src/commands/length.rs new file mode 100644 index 0000000..b4b4b7f --- /dev/null +++ b/crates/sonorust/src/commands/length.rs @@ -0,0 +1,46 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{CommandOptionType, CreateCommand, CreateCommandOption, UserId}; +use setting_inputter::SettingsJson; +use sonorust_db::UserDataMut; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn length(user_id: UserId, length: f64) -> Result { + // 0.1 から 5.0 の範囲外ならその範囲に収める + let length = match length { + ..=0.1 => 0.1, + 5.0.. => 5.0, + _ => length, + }; + + // 小数点以下 1 桁までに制限 + let length_rounded = (length * 10.0).round() / 10.0; + + // ユーザーデータを取得して更新 + { + let mut userdata_mut = UserDataMut::from(user_id).await?; + userdata_mut.length = length_rounded; + + userdata_mut.update().await?; + } + + let lang = SettingsJson::get_bot_lang(); + Ok(format_t!("length.changed", lang, length_rounded)) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("length") + .description(lang_t!("length.command.description", lang)) + .add_option( + CreateCommandOption::new( + CommandOptionType::Number, + lang_t!("length.option.length"), + lang_t!("length.option.length.description", lang), + ) + .min_number_value(0.1) + .max_number_value(5.0) + .required(true), + ) +} diff --git a/crates/sonorust/src/commands/mod.rs b/crates/sonorust/src/commands/mod.rs new file mode 100644 index 0000000..d3dbb70 --- /dev/null +++ b/crates/sonorust/src/commands/mod.rs @@ -0,0 +1,26 @@ +pub mod dict; +pub mod help; +pub mod join; +pub mod leave; +pub mod length; +pub mod model; +pub mod now; +pub mod ping; +pub mod reload; +pub mod server; +pub mod speaker; +pub mod style; +pub mod wav; + +pub use dict::dict; +pub use help::help; +pub use join::join; +pub use leave::leave; +pub use length::length; +pub use model::model; +pub use now::now; +pub use reload::reload; +pub use server::server; +pub use speaker::speaker; +pub use style::style; +pub use wav::wav; diff --git a/crates/sonorust/src/commands/model.rs b/crates/sonorust/src/commands/model.rs new file mode 100644 index 0000000..bd1a922 --- /dev/null +++ b/crates/sonorust/src/commands/model.rs @@ -0,0 +1,96 @@ +use std::ops::Deref; + +use langrustang::lang_t; +use sbv2_api::{Sbv2ModelInfo, SBV2_MODELINFO}; +use serenity::all::{ + ButtonStyle, CreateActionRow, CreateButton, CreateCommand, CreateEmbed, CreateSelectMenu, + CreateSelectMenuKind, CreateSelectMenuOption, +}; +use setting_inputter::SettingsJson; + +use crate::crate_extensions::SettingsJsonExtension; + +pub async fn model() -> (CreateEmbed, Vec) { + // API のモデルデータを取得 + let lock = SBV2_MODELINFO.read().unwrap(); + let sbv2_modelinfo = lock.deref(); + + // embed とプルダウンリスト作成 + let embed = create_embed(sbv2_modelinfo); + let select_menu = create_select_menu(sbv2_modelinfo); + + // コンポーネントの行を作成 + let row0 = CreateActionRow::SelectMenu(select_menu); + let mut components_vec = vec![row0]; + + // モデルの数が 26 以上ならページ移動ボタンを追加 + if sbv2_modelinfo.id_to_model.len() >= 26 { + components_vec.push(create_button_row()); + } + + (embed, components_vec) +} + +fn create_embed(apimodelinfo: &Sbv2ModelInfo) -> CreateEmbed { + // model 25個分の表示を作成 25個以下だったらそこで終了 + let mut content = String::new(); + for i in 0..=24 { + match apimodelinfo.id_to_model.get(&i) { + Some(model) => { + let text = format!("{}: {}\n", i + 1, model.model_name); + content += &text + } + None => break, + } + } + + CreateEmbed::new() + .title("使用できるモデル一覧") + .description(content) +} + +fn create_select_menu(apimodelinfo: &Sbv2ModelInfo) -> CreateSelectMenu { + // model 25個までのプルダウンリストを作成 + let mut selectoption_vec = vec![]; + for i in 0..=24 { + match apimodelinfo.id_to_model.get(&i) { + Some(model) => selectoption_vec.push(CreateSelectMenuOption::new( + model.model_name.as_str(), + model.model_name.as_str(), + )), + None => break, + } + } + + CreateSelectMenu::new( + lang_t!("customid.select.model"), + CreateSelectMenuKind::String { + options: selectoption_vec, + }, + ) +} + +fn create_button_row() -> CreateActionRow { + let page_back = CreateButton::new(lang_t!("customid.page.model.back")) + .label("<-") + .style(ButtonStyle::Primary) + .disabled(true); + + let page_number = CreateButton::new(lang_t!("customid.page.model.number")) + .label("1") + .style(ButtonStyle::Secondary) + .disabled(true); + + let page_forward = CreateButton::new(lang_t!("customid.page.model.forward")) + .label("->") + .style(ButtonStyle::Primary) + .disabled(false); + + CreateActionRow::Buttons(vec![page_back, page_number, page_forward]) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("model").description(lang_t!("model.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/now.rs b/crates/sonorust/src/commands/now.rs new file mode 100644 index 0000000..5551ad2 --- /dev/null +++ b/crates/sonorust/src/commands/now.rs @@ -0,0 +1,50 @@ +use langrustang::{format_t, lang_t}; +use sbv2_api::Sbv2Client; +use serenity::all::{CreateCommand, CreateEmbed, User}; +use setting_inputter::SettingsJson; +use sonorust_db::UserData; + +use crate::{ + crate_extensions::{sbv2_api::Sbv2ClientExtension, SettingsJsonExtension}, + errors::SonorustError, +}; + +pub async fn now(user: &User) -> Result { + let user_data = UserData::from(user.id).await?; + let lang = SettingsJson::get_bot_lang(); + + let valid_model = Sbv2Client::get_valid_model_from_userdata(&user_data); + + // embed の内容設定 + let model_name = &valid_model.model_name; + let speaker_name = &valid_model.speaker_name; + let style_name = &valid_model.style_name; + let length = user_data.length; + + let fields = [ + (lang_t!("now.embed.model", lang), model_name, false), + (lang_t!("now.embed.speaker", lang), speaker_name, false), + (lang_t!("now.embed.style", lang), style_name, false), + ( + lang_t!("now.embed.speech_rate", lang), + &length.to_string(), + false, + ), + ]; + + // ユーザーの名前を取得 + let username = match &user.global_name { + Some(name) => name, + None => &user.name, + }; + + Ok(CreateEmbed::new() + .title(format_t!("now.embed.title", lang, username)) + .fields(fields)) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("now").description(lang_t!("now.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/ping.rs b/crates/sonorust/src/commands/ping.rs new file mode 100644 index 0000000..b81a34a --- /dev/null +++ b/crates/sonorust/src/commands/ping.rs @@ -0,0 +1,25 @@ +use std::time::Duration; + +use langrustang::{format_t, lang_t}; +use serenity::all::{CreateCommand, CreateEmbed}; +use setting_inputter::SettingsJson; + +use crate::crate_extensions::SettingsJsonExtension; + +pub fn measuring_embed() -> CreateEmbed { + let lang = SettingsJson::get_bot_lang(); + + CreateEmbed::new() + .title(lang_t!("ping.embed.title")) + .description(lang_t!("ping.embed.measuring", lang)) +} + +pub fn measured_embed(elapsed: Duration) -> CreateEmbed { + CreateEmbed::new() + .title(lang_t!("ping.embed.title")) + .description(format_t!("ping.embed.measured", elapsed)) +} + +pub fn create_command() -> CreateCommand { + CreateCommand::new("ping").description(lang_t!("ping.command.description")) +} diff --git a/crates/sonorust/src/commands/reload.rs b/crates/sonorust/src/commands/reload.rs new file mode 100644 index 0000000..a3edfe8 --- /dev/null +++ b/crates/sonorust/src/commands/reload.rs @@ -0,0 +1,34 @@ +use langrustang::lang_t; +use sbv2_api::Sbv2Client; +use serenity::all::{CreateCommand, UserId}; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; + +use crate::{crate_extensions::SettingsJsonExtension, registers::APP_OWNER_ID}; + +pub async fn reload(user_id: UserId) -> anyhow::Result<&'static str> { + let lang = SettingsJson::get_bot_lang(); + + let app_owner_id = { + let lock = APP_OWNER_ID.read().unwrap(); + *lock + }; + + if Some(user_id) != app_owner_id { + return Ok(lang_t!("msg.only_owner", lang)); + } + + let client = { + let lock = SETTINGS_JSON.read().unwrap(); + Sbv2Client::from(&lock.host, lock.port) + }; + + client.update_modelinfo().await?; + + Ok(lang_t!("reload.executed", lang)) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("reload").description(lang_t!("reload.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/server.rs b/crates/sonorust/src/commands/server.rs new file mode 100644 index 0000000..1d08603 --- /dev/null +++ b/crates/sonorust/src/commands/server.rs @@ -0,0 +1,151 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ + Context, CreateActionRow, CreateCommand, CreateEmbed, CreateSelectMenu, CreateSelectMenuKind, + CreateSelectMenuOption, GuildId, UserId, +}; +use setting_inputter::SettingsJson; +use sonorust_db::GuildData; + +use crate::{ + crate_extensions::SettingsJsonExtension, + errors::{NoneToSonorustError, SonorustError}, + registers::APP_OWNER_ID, +}; + +pub async fn server( + ctx: &Context, + guild_id: Option, + user_id: UserId, +) -> Result<(CreateEmbed, Vec), SonorustError> { + // 必要な情報の取得 + let guild_id = guild_id.ok_or_sonorust_err()?; + let user = guild_id.member(&ctx.http, user_id).await?; + let guilddata = GuildData::from(guild_id).await?; + + let bot_owner_id = { + let app_owner_id = APP_OWNER_ID.read().unwrap(); + *app_owner_id + }; + + let is_bot_owner = { bot_owner_id == Some(user_id) }; + + let guild_name = guild_id + .name(&ctx.cache) + .unwrap_or_else(|| "Unknown".to_string()); + + let embed = create_embed(&guild_name, guilddata); + + // 管理者でない、またはbotの所有者でないならセレクトメニューを追加せずに返す + match user.permissions(&ctx.cache) { + Ok(permissons) => { + if !permissons.administrator() && !is_bot_owner { + return Ok((embed, vec![])); + } + } + + Err(_) => return Ok((embed, vec![])), + }; + + return Ok((embed, vec![create_select_menu()])); +} + +fn create_embed(guild_name: &str, guild_data: GuildData) -> CreateEmbed { + let lang = SettingsJson::get_bot_lang(); + + let bool_to_onoff = |bool_: bool| match bool_ { + true => "ON", + false => "OFF", + }; + + let title = format_t!("server.embed.title", lang, guild_name); + + let fields = [ + ( + lang_t!("guild.desc.is_auto_join", lang), + format!("{}", bool_to_onoff(guild_data.options.is_auto_join)), + false, + ), + ( + lang_t!("guild.desc.is_dic_onlyadmin", lang), + format!("{}", bool_to_onoff(guild_data.options.is_dic_onlyadmin)), + false, + ), + ( + lang_t!("guild.desc.is_entrance_exit_log", lang), + format!("{}", bool_to_onoff(guild_data.options.is_entrance_exit_log)), + false, + ), + ( + lang_t!("guild.desc.is_entrance_exit_play", lang), + format!( + "{}", + bool_to_onoff(guild_data.options.is_entrance_exit_play) + ), + false, + ), + ( + lang_t!("guild.desc.is_notice_attachment", lang), + format!("{}", bool_to_onoff(guild_data.options.is_notice_attachment)), + false, + ), + ( + lang_t!("guild.desc.is_if_long_fastread", lang), + format!("{}", bool_to_onoff(guild_data.options.is_if_long_fastread)), + false, + ), + ]; + + CreateEmbed::new().fields(fields).title(title) +} + +fn create_select_menu() -> CreateActionRow { + let lang = SettingsJson::get_bot_lang(); + + let is_auto_join = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_auto_join", lang), + lang_t!("guild.is_auto_join"), + ); + let dic_only_admin = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_dic_onlyadmin", lang), + lang_t!("guild.is_dic_onlyadmin"), + ); + let is_entrance_exit_log = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_entrance_exit_log", lang), + lang_t!("guild.is_entrance_exit_log"), + ); + let is_entrance_exit_play = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_entrance_exit_play", lang), + lang_t!("guild.is_entrance_exit_play"), + ); + let is_notice_attachment = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_notice_attachment", lang), + lang_t!("guild.is_notice_attachment"), + ); + let is_if_long_fastread = CreateSelectMenuOption::new( + lang_t!("guild.desc.is_if_long_fastread", lang), + lang_t!("guild.is_if_long_fastread"), + ); + + let select_menu = CreateSelectMenu::new( + lang_t!("customid.change_server_settings"), + CreateSelectMenuKind::String { + options: vec![ + is_auto_join, + dic_only_admin, + is_entrance_exit_log, + is_entrance_exit_play, + is_notice_attachment, + is_if_long_fastread, + ], + }, + ) + .placeholder(lang_t!("server.components.placeholder", lang)); + + CreateActionRow::SelectMenu(select_menu) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("server").description(lang_t!("server.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/speaker.rs b/crates/sonorust/src/commands/speaker.rs new file mode 100644 index 0000000..cb6fa9e --- /dev/null +++ b/crates/sonorust/src/commands/speaker.rs @@ -0,0 +1,108 @@ +use std::borrow::Borrow; + +use langrustang::{format_t, lang_t}; +use sbv2_api::{ModelInfo, SBV2_MODELINFO}; +use serenity::all::{ + ButtonStyle, CreateActionRow, CreateButton, CreateCommand, CreateEmbed, CreateSelectMenu, + CreateSelectMenuKind, CreateSelectMenuOption, UserId, +}; +use setting_inputter::SettingsJson; +use sonorust_db::UserData; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn speaker( + user_id: UserId, +) -> Result<(CreateEmbed, Vec), SonorustError> { + let userdata = UserData::from(user_id).await?; + + // API のモデルデータを取得 + let lock = SBV2_MODELINFO.read().unwrap(); + let sbv2_modelinfo = lock.borrow(); + + // そのユーザーのモデルを取得、ないなら id が 0 の物を取得 + let model = match sbv2_modelinfo.name_to_model.get(&userdata.model_name) { + Some(model) => model, + None => sbv2_modelinfo.id_to_model.get(&0).unwrap(), + }; + + // embed とプルダウンリスト作成 + let embed = create_embed(&model); + let select_menu = create_select_menu(&model); + + // コンポーネントの行を作成 + let row0 = CreateActionRow::SelectMenu(select_menu); + let mut components_vec = vec![row0]; + + // 話者の数が 26 以上ならページ移動ボタンを追加 + if model.id2spk.len() >= 26 { + components_vec.push(create_button_row()); + } + + Ok((embed, components_vec)) +} + +fn create_embed(model: &ModelInfo) -> CreateEmbed { + // model 25個分の表示を作成 25個以下だったらそこで終了 + let mut content = String::new(); + for i in 0..=24 { + match model.id2spk.get(&i) { + Some(spk_name) => { + let text = format!("{}: {}\n", i + 1, spk_name); + content += &text + } + None => break, + } + } + + let lang = SettingsJson::get_bot_lang(); + let title = format_t!("speaker.embed.title", lang, model.model_name); + + CreateEmbed::new().title(title).description(content) +} + +fn create_select_menu(model: &ModelInfo) -> CreateSelectMenu { + // model 25個までのプルダウンリストを作成 + let mut selectoption_vec = vec![]; + for i in 0..=24 { + match model.id2spk.get(&i) { + Some(spk_name) => selectoption_vec.push(CreateSelectMenuOption::new( + spk_name.as_str(), + format!("{}||{}", model.model_name, spk_name), + )), + None => break, + } + } + + CreateSelectMenu::new( + lang_t!("customid.select.speaker"), + CreateSelectMenuKind::String { + options: selectoption_vec, + }, + ) +} + +fn create_button_row() -> CreateActionRow { + let page_back = CreateButton::new(lang_t!("customid.page.speaker.back")) + .label("<-") + .style(ButtonStyle::Primary) + .disabled(true); + + let page_number = CreateButton::new(lang_t!("customid.page.speaker.number")) + .label("1") + .style(ButtonStyle::Secondary) + .disabled(true); + + let page_forward = CreateButton::new(lang_t!("customid.page.speaker.forward")) + .label("->") + .style(ButtonStyle::Primary) + .disabled(false); + + CreateActionRow::Buttons(vec![page_back, page_number, page_forward]) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("speaker").description(lang_t!("speaker.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/style.rs b/crates/sonorust/src/commands/style.rs new file mode 100644 index 0000000..c4580ed --- /dev/null +++ b/crates/sonorust/src/commands/style.rs @@ -0,0 +1,106 @@ +use std::borrow::Borrow; + +use langrustang::{format_t, lang_t}; +use sbv2_api::{ModelInfo, SBV2_MODELINFO}; +use serenity::all::{ + ButtonStyle, CreateActionRow, CreateButton, CreateCommand, CreateEmbed, CreateSelectMenu, + CreateSelectMenuKind, CreateSelectMenuOption, UserId, +}; +use setting_inputter::SettingsJson; +use sonorust_db::UserData; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn style(user_id: UserId) -> Result<(CreateEmbed, Vec), SonorustError> { + let userdata = UserData::from(user_id).await?; + + // API のモデルデータを取得 + let lock = SBV2_MODELINFO.read().unwrap(); + let sbv2_modelinfo = lock.borrow(); + + // そのユーザーのモデルを取得、ないなら id が 0 の物を取得 + let model = match sbv2_modelinfo.name_to_model.get(&userdata.model_name) { + Some(model) => model, + None => sbv2_modelinfo.id_to_model.get(&0).unwrap(), + }; + + // embed とプルダウンリスト作成 + let embed = create_embed(&model); + let select_menu = create_select_menu(&model); + + // コンポーネントの行を作成 + let row0 = CreateActionRow::SelectMenu(select_menu); + let mut components_vec = vec![row0]; + + // スタイルの数が 26 以上ならページ移動ボタンを追加 + if model.id2style.len() >= 26 { + components_vec.push(create_button_row()); + } + + Ok((embed, components_vec)) +} + +fn create_embed(model: &ModelInfo) -> CreateEmbed { + // model 25個分の表示を作成 25個以下だったらそこで終了 + let mut content = String::new(); + for i in 0..=24 { + match model.id2style.get(&i) { + Some(spk_name) => { + let text = format!("{}: {}\n", i + 1, spk_name); + content += &text + } + None => break, + } + } + + let lang = SettingsJson::get_bot_lang(); + let title = format_t!("style.embed.title", lang, model.model_name); + + CreateEmbed::new().title(title).description(content) +} + +fn create_select_menu(model: &ModelInfo) -> CreateSelectMenu { + // model 25個までのプルダウンリストを作成 + let mut selectoption_vec = vec![]; + for i in 0..=24 { + match model.id2style.get(&i) { + Some(style_name) => selectoption_vec.push(CreateSelectMenuOption::new( + style_name.as_str(), + format!("{}||{}", model.model_name, style_name), + )), + None => break, + } + } + + CreateSelectMenu::new( + lang_t!("customid.select.style"), + CreateSelectMenuKind::String { + options: selectoption_vec, + }, + ) +} + +fn create_button_row() -> CreateActionRow { + let page_back = CreateButton::new(lang_t!("customid.page.style.forward")) + .label("<-") + .style(ButtonStyle::Primary) + .disabled(true); + + let page_number = CreateButton::new(lang_t!("customid.page.style.number")) + .label("1") + .style(ButtonStyle::Secondary) + .disabled(true); + + let page_forward = CreateButton::new(lang_t!("customid.page.style.back")) + .label("->") + .style(ButtonStyle::Primary) + .disabled(false); + + CreateActionRow::Buttons(vec![page_back, page_number, page_forward]) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("style").description(lang_t!("style.command.description", lang)) +} diff --git a/crates/sonorust/src/commands/wav.rs b/crates/sonorust/src/commands/wav.rs new file mode 100644 index 0000000..3bd6a67 --- /dev/null +++ b/crates/sonorust/src/commands/wav.rs @@ -0,0 +1,42 @@ +use langrustang::lang_t; +use sbv2_api::Sbv2Client; +use serenity::all::{ + CommandOptionType, CreateAttachment, CreateCommand, CreateCommandOption, UserId, +}; +use setting_inputter::SettingsJson; +use sonorust_db::UserData; + +use crate::{ + crate_extensions::{sbv2_api::Sbv2ClientExtension as _, SettingsJsonExtension}, + errors::SonorustError, +}; + +pub async fn wav( + user_id: UserId, + text: &str, +) -> Result<(Option, Option<&str>), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + let userdata = UserData::from(user_id).await?; + + let Ok(voice_data) = Sbv2Client::infer_from_user(text, &userdata).await else { + log::error!(lang_t!("log.fail_inter_not_launch")); + return Ok((None, Some(lang_t!("wav.fail_infer", lang)))); + }; + + Ok((Some(CreateAttachment::bytes(voice_data, "audio.mp3")), None)) +} + +pub fn create_command() -> CreateCommand { + let lang = SettingsJson::get_bot_lang(); + + CreateCommand::new("wav") + .description(lang_t!("wav.command.description", lang)) + .add_option( + CreateCommandOption::new( + CommandOptionType::String, + lang_t!("wav.option.content"), + lang_t!("wav.option.content.description", lang), + ) + .required(true), + ) +} diff --git a/crates/sonorust/src/components/button/dict_add.rs b/crates/sonorust/src/components/button/dict_add.rs new file mode 100644 index 0000000..c97455e --- /dev/null +++ b/crates/sonorust/src/components/button/dict_add.rs @@ -0,0 +1,97 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, Context, CreateQuickModal, ModalInteraction}; +use setting_inputter::SettingsJson; +use sonorust_db::{GuildData, GuildDataMut}; + +use crate::{ + crate_extensions::SettingsJsonExtension, + errors::{NoneToSonorustError, SonorustError}, + registers::APP_OWNER_ID, +}; + +pub async fn dict_add( + ctx: &Context, + interaction: &ComponentInteraction, +) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + let guild_id = interaction.guild_id.ok_or_sonorust_err()?; + let guild_data = GuildData::from(guild_id).await?; + + let bot_owner_id = { + let lock = APP_OWNER_ID.read().unwrap(); + *lock + }; + + let inter_member = guild_id.member(&ctx.http, interaction.user.id).await?; + + let is_admin = inter_member.permissions(&ctx.cache)?.administrator(); + let is_bot_owner = { bot_owner_id == Some(interaction.user.id) }; + + // サーバー辞書の編集が管理者に限定されていた場合 + // もしサーバーの管理者でないなら返す (BOT の所有者の場合許可) + let is_dic_adminonly = guild_data.options.is_dic_onlyadmin; + + if (is_dic_adminonly && !is_admin) && !is_bot_owner { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = lang_t!("msg.only_admin", lang), + ephemeral = true + ) + .await?; + + return Ok(()); + } + + // modal の送信と処理 + let modal = create_quickmodal(); + let Ok(Some(response)) = interaction.quick_modal(ctx, modal).await else { + return Ok(()); + }; + + let inputs = response.inputs; + on_submit(ctx, &response.interaction, inputs).await?; + + Ok(()) +} + +pub fn create_quickmodal() -> CreateQuickModal { + let lang = SettingsJson::get_bot_lang(); + + CreateQuickModal::new(lang_t!("dict.modal.add.title", lang)) + .timeout(std::time::Duration::from_secs(600)) + .short_field(lang_t!("dict.modal.add.word", lang)) + .short_field(lang_t!("dict.modal.add.readings", lang)) +} + +async fn on_submit( + ctx: &Context, + interaction: &ModalInteraction, + inputs: Vec, +) -> Result<(), SonorustError> { + // 入力内容の取得 + let (key, value) = (&inputs[0], &inputs[1]); + + let guild_id = interaction.guild_id.ok_or_sonorust_err()?; + + { + let mut guild_data_mut = GuildDataMut::from(guild_id).await?; + guild_data_mut.dict.insert(key.clone(), value.clone()); + + guild_data_mut.update().await?; + } + + let lang = SettingsJson::get_bot_lang(); + + // 返答するメッセージを作成 + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = format_t!("dict.modal.add.set", lang, key, value), + ephemeral = true + ) + .await?; + + Ok(()) +} diff --git a/crates/sonorust/src/components/button/dict_remove.rs b/crates/sonorust/src/components/button/dict_remove.rs new file mode 100644 index 0000000..5f64a85 --- /dev/null +++ b/crates/sonorust/src/components/button/dict_remove.rs @@ -0,0 +1,111 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, Context, CreateQuickModal, ModalInteraction}; +use setting_inputter::SettingsJson; +use sonorust_db::{GuildData, GuildDataMut}; + +use crate::{ + crate_extensions::SettingsJsonExtension, + errors::{NoneToSonorustError, SonorustError}, + registers::APP_OWNER_ID, +}; + +pub async fn dict_remove( + ctx: &Context, + interaction: &ComponentInteraction, +) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + let guild_id = interaction.guild_id.ok_or_sonorust_err()?; + let guild_data = GuildData::from(guild_id).await?; + + let bot_owner_id = { + let lock = APP_OWNER_ID.read().unwrap(); + *lock + }; + + let inter_member = guild_id.member(&ctx.http, interaction.user.id).await?; + + let is_admin = inter_member.permissions(&ctx.cache)?.administrator(); + let is_bot_owner = { bot_owner_id == Some(interaction.user.id) }; + + // サーバー辞書の編集が管理者に限定されていた場合 + // もしサーバーの管理者でないなら返す (BOT の所有者の場合許可) + let is_dic_adminonly = guild_data.options.is_dic_onlyadmin; + + if (is_dic_adminonly && !is_admin) && !is_bot_owner { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = lang_t!("msg.only_admin", lang), + ephemeral = true, + ) + .await?; + + return Ok(()); + } + + // modal の送信と処理 + let modal = create_quickmodal(); + let Ok(Some(response)) = interaction.quick_modal(ctx, modal).await else { + return Ok(()); + }; + + let inputs = response.inputs; + on_submit(ctx, &response.interaction, inputs).await?; + + Ok(()) +} + +pub fn create_quickmodal() -> CreateQuickModal { + let lang = SettingsJson::get_bot_lang(); + + CreateQuickModal::new(lang_t!("dict.modal.remove.title", lang)) + .timeout(std::time::Duration::from_secs(600)) + .short_field(lang_t!("dict.modal.remove.field", lang)) +} + +async fn on_submit( + ctx: &Context, + interaction: &ModalInteraction, + inputs: Vec, +) -> Result<(), SonorustError> { + // 入力内容の取得 + let key = &inputs[0]; + + let guild_id = interaction.guild_id.ok_or_sonorust_err()?; + + let removed = { + let mut guild_data_mut = GuildDataMut::from(guild_id).await?; + let removed = guild_data_mut.dict.remove(key); + + if removed.is_some() { + guild_data_mut.update().await?; + } + removed + }; + + let lang = SettingsJson::get_bot_lang(); + + // 返答するメッセージを作成\ + match removed { + Some(_) => { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = format_t!("dict.modal.remove.deleted", lang, key) + ) + .await?; + } + None => { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = format_t!("dict.modal.remove.not_found", lang, key), + ephemeral = true, + ) + .await?; + } + }; + + Ok(()) +} diff --git a/crates/sonorust/src/components/button/mod.rs b/crates/sonorust/src/components/button/mod.rs new file mode 100644 index 0000000..bc1e646 --- /dev/null +++ b/crates/sonorust/src/components/button/mod.rs @@ -0,0 +1,7 @@ +pub mod dict_add; +pub mod dict_remove; +pub mod move_page; + +pub use dict_add::dict_add; +pub use dict_remove::dict_remove; +pub use move_page::move_page; diff --git a/crates/sonorust/src/components/button/move_page.rs b/crates/sonorust/src/components/button/move_page.rs new file mode 100644 index 0000000..756a1d2 --- /dev/null +++ b/crates/sonorust/src/components/button/move_page.rs @@ -0,0 +1,420 @@ +use std::{borrow::Borrow, collections::HashMap}; + +use langrustang::{format_t, lang_t}; +use sbv2_api::{Sbv2Client, Sbv2ModelInfo, SBV2_MODELINFO}; +use serenity::all::{ + ButtonStyle, ComponentInteraction, Context, CreateActionRow, CreateButton, CreateEmbed, + CreateSelectMenu, CreateSelectMenuKind, CreateSelectMenuOption, +}; +use setting_inputter::SettingsJson; +use sonorust_db::UserData; + +use crate::{ + crate_extensions::{sbv2_api::Sbv2ClientExtension, SettingsJsonExtension}, + errors::SonorustError, +}; + +pub async fn move_page( + ctx: &Context, + interaction: &ComponentInteraction, + custom_id: &str, +) -> Result<(), SonorustError> { + // ユーザーデータとモデルを取得する + let user_data = UserData::from(interaction.user.id).await?; + + // custom_id ごとに要素を作成 + let (embed, select_menu, button_row) = { + let lock = SBV2_MODELINFO.read().unwrap(); + let sbv2_modelinfo = lock.borrow(); + + let button_row = &interaction.message.components[1]; + let page_button = match &button_row.components[1] { + serenity::all::ActionRowComponent::Button(button) => button, + _ => unreachable!(), + }; + + let current_page = page_button + .label + .as_ref() + .map(|i| i.parse().unwrap_or(0)) + .unwrap_or(0); + + match custom_id { + lang_t!("customid.page.model.forward") => { + model_pageforward(sbv2_modelinfo, current_page) + } + lang_t!("customid.page.model.back") => model_pageback(sbv2_modelinfo, current_page), + + lang_t!("customid.page.speaker.forward") => { + speaker_pageforward(&user_data, sbv2_modelinfo, current_page) + } + lang_t!("customid.page.speaker.back") => { + speaker_pageback(&user_data, sbv2_modelinfo, current_page) + } + + lang_t!("customid.page.style.forward") => { + style_pageforward(&user_data, sbv2_modelinfo, current_page) + } + lang_t!("customid.page.style.back") => { + style_pageback(&user_data, sbv2_modelinfo, current_page) + } + _ => unreachable!(), + } + }; + + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = vec![CreateActionRow::SelectMenu(select_menu), button_row], + ) + .await?; + Ok(()) +} + +/* custom_id ごとの動作 */ + +fn model_pageforward( + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let map: HashMap<_, _> = sbv2_modelinfo + .id_to_model + .iter() + .map(|(k, v)| (*k, v.model_name.as_str())) + .collect(); + + let embed = create_embed(lang_t!("model.embed.title", lang), &map, current_page); + let select_menu = + create_select_menu_model(lang_t!("customid.select.model"), &map, current_page); + let button_row = craete_button_row_forward( + current_page, + &map, + lang_t!("customid.page.model.back"), + lang_t!("customid.page.model.number"), + lang_t!("customid.page.model.forward"), + ); + + (embed, select_menu, button_row) +} + +fn model_pageback( + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let page = current_page - 2; + let map: HashMap<_, _> = sbv2_modelinfo + .id_to_model + .iter() + .map(|(k, v)| (*k, v.model_name.as_str())) + .collect(); + + let embed = create_embed(lang_t!("model.embed.title", lang), &map, page); + let select_menu = create_select_menu_model(lang_t!("customid.select.model"), &map, page); + let button_row = craete_button_row_back( + current_page, + lang_t!("customid.page.model.back"), + lang_t!("customid.page.model.number"), + lang_t!("customid.page.model.forward"), + ); + + (embed, select_menu, button_row) +} + +fn speaker_pageforward( + userdata: &UserData, + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let valid_model = Sbv2Client::get_valid_model_from_userdata(userdata); + let model = sbv2_modelinfo + .id_to_model + .get(&valid_model.model_id) + .unwrap_or_else(|| sbv2_modelinfo.id_to_model.get(&0).unwrap()); + + let map: HashMap<_, _> = model.id2spk.iter().map(|(k, v)| (*k, v.as_str())).collect(); + + let embed = create_embed( + format_t!("speaker.embed.title", lang, model.model_name), + &map, + current_page, + ); + let select_menu = create_select_menu_except_model( + lang_t!("customid.select.speaker"), + &model.model_name, + &map, + current_page, + ); + let button_row = craete_button_row_forward( + current_page, + &map, + lang_t!("customid.page.speaker.back"), + lang_t!("customid.page.speaker.number"), + lang_t!("customid.page.speaker.forward"), + ); + + (embed, select_menu, button_row) +} + +fn speaker_pageback( + userdata: &UserData, + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let valid_model = Sbv2Client::get_valid_model_from_userdata(userdata); + let page = current_page - 2; + let model = sbv2_modelinfo + .id_to_model + .get(&valid_model.model_id) + .unwrap_or_else(|| sbv2_modelinfo.id_to_model.get(&0).unwrap()); + + let map: HashMap<_, _> = model.id2spk.iter().map(|(k, v)| (*k, v.as_str())).collect(); + + let embed = create_embed( + format_t!("speaker.embed.title", lang, model.model_name), + &map, + page, + ); + let select_menu = create_select_menu_except_model( + lang_t!("customid.select.speaker"), + &model.model_name, + &map, + page, + ); + let button_row = craete_button_row_back( + current_page, + lang_t!("customid.page.speaker.back"), + lang_t!("customid.page.speaker.number"), + lang_t!("customid.page.speaker.forward"), + ); + + (embed, select_menu, button_row) +} + +fn style_pageforward( + userdata: &UserData, + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let valid_model = Sbv2Client::get_valid_model_from_userdata(userdata); + let model = sbv2_modelinfo + .id_to_model + .get(&valid_model.model_id) + .unwrap_or_else(|| sbv2_modelinfo.id_to_model.get(&0).unwrap()); + + let map: HashMap<_, _> = model + .id2style + .iter() + .map(|(k, v)| (*k, v.as_str())) + .collect(); + + let embed = create_embed( + format_t!("style.embed.title", lang, model.model_name), + &map, + current_page, + ); + let select_menu = create_select_menu_except_model( + lang_t!("customid.select.style"), + &model.model_name, + &map, + current_page, + ); + let button_row = craete_button_row_forward( + current_page, + &map, + lang_t!("customid.page.style.forward"), + lang_t!("customid.page.style.number"), + lang_t!("customid.page.style.back"), + ); + + (embed, select_menu, button_row) +} + +fn style_pageback( + userdata: &UserData, + sbv2_modelinfo: &Sbv2ModelInfo, + current_page: u32, +) -> (CreateEmbed, CreateSelectMenu, CreateActionRow) { + let lang = SettingsJson::get_bot_lang(); + + let valid_model = Sbv2Client::get_valid_model_from_userdata(userdata); + let page = current_page - 2; + let model = sbv2_modelinfo + .id_to_model + .get(&valid_model.model_id) + .unwrap_or_else(|| sbv2_modelinfo.id_to_model.get(&0).unwrap()); + + let map: HashMap<_, _> = model + .id2style + .iter() + .map(|(k, v)| (*k, v.as_str())) + .collect(); + + let embed = create_embed( + format_t!("style.embed.title", lang, model.model_name), + &map, + page, + ); + let select_menu = create_select_menu_except_model( + lang_t!("customid.select.style"), + &model.model_name, + &map, + page, + ); + let button_row = craete_button_row_back( + current_page, + lang_t!("customid.page.style.forward"), + lang_t!("customid.page.style.number"), + lang_t!("customid.page.style.back"), + ); + + (embed, select_menu, button_row) +} + +/* embed, select_menu, button 作成部分の処理 */ + +fn create_embed(title: S, map: &HashMap, page: u32) -> CreateEmbed +where + S: Into, +{ + // 25個分の表示を作成 25個以下だったらそこで終了 + // page数 * 25 を足して次のページを取得する + let mut content = String::new(); + for i in (page * 25)..=24 + (page * 25) { + match map.get(&i) { + Some(value) => { + let text = format!("{}: {}\n", i + 1, value); + content += &text + } + None => break, + } + } + + CreateEmbed::new().title(title).description(content) +} + +fn create_select_menu_model( + custom_id: &str, + map: &HashMap, + page: u32, +) -> CreateSelectMenu { + // 25個までのプルダウンリストを作成 + // page数 * 25 を足して次のページを取得する + let mut selectoption_vec = vec![]; + for i in (page * 25)..=24 + (page * 25) { + match map.get(&i) { + Some(str_) => selectoption_vec.push(CreateSelectMenuOption::new(*str_, *str_)), + None => break, + } + } + CreateSelectMenu::new( + custom_id, + CreateSelectMenuKind::String { + options: selectoption_vec, + }, + ) +} + +fn create_select_menu_except_model( + custom_id: &str, + model_name: &str, + map: &HashMap, + page: u32, +) -> CreateSelectMenu { + // 25個までのプルダウンリストを作成 + // page数 * 25 を足して次のページを取得する + let mut selectoption_vec = vec![]; + for i in (page * 25)..=24 + (page * 25) { + match map.get(&i) { + Some(str_) => selectoption_vec.push(CreateSelectMenuOption::new( + *str_, + format!("{}||{}", model_name, str_), + )), + None => break, + } + } + CreateSelectMenu::new( + custom_id, + CreateSelectMenuKind::String { + options: selectoption_vec, + }, + ) +} + +fn craete_button_row_forward( + current_page: u32, + map: &HashMap, + custom_id_back: &str, + custom_id_num: &str, + custom_id_forward: &str, +) -> CreateActionRow { + let next_page = current_page + 1; + let nextpage_map_number = next_page * 25; + + // 次のページがないなら page_forward ボタンを無効化する + let is_nextpage_exists = { + match map.get(&nextpage_map_number) { + Some(_) => false, + None => true, + } + }; + + let page_back = CreateButton::new(custom_id_back) + .label("<-") + .style(ButtonStyle::Primary) + .disabled(false); + + let page_number = CreateButton::new(custom_id_num) + .label(next_page.to_string()) + .style(ButtonStyle::Secondary) + .disabled(true); + + let page_forward = CreateButton::new(custom_id_forward) + .label("->") + .style(ButtonStyle::Primary) + .disabled(is_nextpage_exists); + + CreateActionRow::Buttons(vec![page_back, page_number, page_forward]) +} + +fn craete_button_row_back( + current_page: u32, + custom_id_back: &str, + custom_id_num: &str, + custom_id_forward: &str, +) -> CreateActionRow { + let prev_page = current_page - 1; + + // 1ページ目なら page_back ボタンを無効化する + let is_prevpage_exists = match prev_page { + 1 => true, + _ => false, + }; + + let page_back = CreateButton::new(custom_id_back) + .label("<-") + .style(ButtonStyle::Primary) + .disabled(is_prevpage_exists); + + let page_number = CreateButton::new(custom_id_num) + .label(prev_page.to_string()) + .style(ButtonStyle::Secondary) + .disabled(true); + + let page_forward = CreateButton::new(custom_id_forward) + .label("->") + .style(ButtonStyle::Primary) + .disabled(false); + + CreateActionRow::Buttons(vec![page_back, page_number, page_forward]) +} diff --git a/crates/sonorust/src/components/mod.rs b/crates/sonorust/src/components/mod.rs new file mode 100644 index 0000000..3c5ee4d --- /dev/null +++ b/crates/sonorust/src/components/mod.rs @@ -0,0 +1,2 @@ +pub mod button; +pub mod select_menu; diff --git a/crates/sonorust/src/components/select_menu/mod.rs b/crates/sonorust/src/components/select_menu/mod.rs new file mode 100644 index 0000000..c59a497 --- /dev/null +++ b/crates/sonorust/src/components/select_menu/mod.rs @@ -0,0 +1,9 @@ +pub mod model; +pub mod server; +pub mod speaker; +pub mod style; + +pub use model::model; +pub use server::server; +pub use speaker::speaker; +pub use style::style; diff --git a/crates/sonorust/src/components/select_menu/model.rs b/crates/sonorust/src/components/select_menu/model.rs new file mode 100644 index 0000000..0fb82a3 --- /dev/null +++ b/crates/sonorust/src/components/select_menu/model.rs @@ -0,0 +1,48 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, ComponentInteractionDataKind, Context}; +use setting_inputter::SettingsJson; +use sonorust_db::UserDataMut; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn model(ctx: &Context, interaction: &ComponentInteraction) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + // 返信する処理 + let send_msg = |content| { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = content, + ephemeral = true + ) + }; + + // 選択したモデル名を取得 + let choice_model = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { values } => &values[0], + _ => { + log::error!(lang_t!("log.fail_get_data")); + send_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + } + }; + + // ユーザーデータを更新 + { + let mut user_data_mut = UserDataMut::from(interaction.user.id).await?; + + user_data_mut.model_name = choice_model.to_string(); + user_data_mut.speaker_name = String::default(); + user_data_mut.style_name = String::default(); + + user_data_mut.update().await?; + } + + // 返答するメッセージを作成 + let content = format_t!("model.changed", lang, choice_model); + + // メッセージを送信 + send_msg(&content).await?; + Ok(()) +} diff --git a/crates/sonorust/src/components/select_menu/server.rs b/crates/sonorust/src/components/select_menu/server.rs new file mode 100644 index 0000000..201ae63 --- /dev/null +++ b/crates/sonorust/src/components/select_menu/server.rs @@ -0,0 +1,157 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, ComponentInteractionDataKind, Context, EditMessage}; +use setting_inputter::SettingsJson; +use sonorust_db::GuildDataMut; + +use crate::{ + crate_extensions::SettingsJsonExtension, + errors::{NoneToSonorustError, SonorustError}, + registers::APP_OWNER_ID, +}; + +pub async fn server( + ctx: &Context, + interaction: &ComponentInteraction, +) -> Result<(), SonorustError> { + let guild_id = interaction.guild_id.ok_or_sonorust_err()?; + let inter_member = guild_id.member(&ctx.http, interaction.user.id).await?; + + let lang = SettingsJson::get_bot_lang(); + + let bot_owner_id = { + let app_owner_id = APP_OWNER_ID.read().unwrap(); + *app_owner_id + }; + + let is_admin = inter_member.permissions(&ctx.cache)?.administrator(); + let is_bot_owner = { bot_owner_id == Some(interaction.user.id) }; + + let send_ephemeral_msg = |content: &str| { + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = content, + ephemeral = true + ) + }; + + // 選択した値を取得 + let choice_value = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { values } => values[0].as_str(), + + _ => { + log::error!(lang_t!("log.fail_get_data")); + send_ephemeral_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + } + }; + + // 管理者でもbotの所有者でもなければ + if !is_admin && !is_bot_owner { + send_ephemeral_msg(lang_t!("msg.only_admin", lang)).await?; + } + + // サーバーデータの更新 + let new_bool = { + let mut guilddata_mut = GuildDataMut::from(guild_id).await?; + + // 選択した値によってサーバーデータを編集して、変化後の値を取得する + let change_value = |ref_bool: &mut bool| { + *ref_bool = !*ref_bool; + *ref_bool + }; + + let new_bool = match choice_value { + lang_t!("guild.is_auto_join") => change_value(&mut guilddata_mut.options.is_auto_join), + lang_t!("guild.is_dic_onlyadmin") => { + change_value(&mut guilddata_mut.options.is_dic_onlyadmin) + } + lang_t!("guild.is_entrance_exit_log") => { + change_value(&mut guilddata_mut.options.is_entrance_exit_log) + } + lang_t!("guild.is_entrance_exit_play") => { + change_value(&mut guilddata_mut.options.is_entrance_exit_play) + } + lang_t!("guild.is_notice_attachment") => { + change_value(&mut guilddata_mut.options.is_notice_attachment) + } + lang_t!("guild.is_if_long_fastread") => { + change_value(&mut guilddata_mut.options.is_if_long_fastread) + } + + _ => { + log::error!("{}", lang_t!("log.not_implemented_customid")); + send_ephemeral_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + } + }; + + guilddata_mut.update().await?; + new_bool + }; + + let new_bool_value = match new_bool { + true => "ON", + false => "OFF", + }; + + // 元の メッセージと embed を取得して選択された値を変更 + let choice_value_title = match choice_value { + lang_t!("guild.is_auto_join") => lang_t!("guild.desc.is_auto_join", lang), + lang_t!("guild.is_dic_onlyadmin") => lang_t!("guild.desc.is_dic_onlyadmin", lang), + lang_t!("guild.is_entrance_exit_log") => lang_t!("guild.desc.is_entrance_exit_log", lang), + lang_t!("guild.is_entrance_exit_play") => lang_t!("guild.desc.is_entrance_exit_play", lang), + lang_t!("guild.is_notice_attachment") => lang_t!("guild.desc.is_notice_attachment", lang), + lang_t!("guild.is_if_long_fastread") => lang_t!("guild.desc.is_if_long_fastread", lang), + + _ => { + log::error!("{}", lang_t!("log.not_implemented_customid")); + send_ephemeral_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + } + }; + + let mut interaction_msg = interaction + .channel_id + .message(&ctx.http, interaction.message.id) + .await?; + + // embed の取得と書き換え + let Some(mut embed) = interaction_msg.embeds.get(0).cloned() else { + log::error!("{}", lang_t!("log.fail_get_data")); + send_ephemeral_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + }; + + let Some(field_value) = embed + .fields + .iter_mut() + .filter(|i| i.name == choice_value_title) + .map(|i| &mut i.value) + .next() + else { + log::error!("{}", lang_t!("log.fail_get_data")); + send_ephemeral_msg(lang_t!("msg.failed.get", lang)).await?; + return Ok(()); + }; + + *field_value = new_bool_value.to_string(); + + // メッセージの編集 + let edit_message = EditMessage::new().embed(embed.into()); + let task_edit_message = interaction_msg.edit(&ctx.http, edit_message); + + // 返信用のメッセージを送信 + let task_create_response = eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = format_t!("server.changed", lang, choice_value_title, new_bool_value), + ephemeral = false, + ); + + let (result1, result2) = tokio::join!(task_edit_message, task_create_response); + result1?; + result2?; + + Ok(()) +} diff --git a/crates/sonorust/src/components/select_menu/speaker.rs b/crates/sonorust/src/components/select_menu/speaker.rs new file mode 100644 index 0000000..5d5d784 --- /dev/null +++ b/crates/sonorust/src/components/select_menu/speaker.rs @@ -0,0 +1,71 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, ComponentInteractionDataKind, Context}; +use setting_inputter::SettingsJson; +use sonorust_db::UserDataMut; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn speaker( + ctx: &Context, + interaction: &ComponentInteraction, +) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + // 選択した値の取得 + let choice_value = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { values } => &values[0], + _ => { + log::error!(lang_t!("log.fail_get_data")); + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = lang_t!("msg.failed.get", lang), + ephemeral = true, + ) + .await?; + + return Ok(()); + } + }; + + // 選択したモデルと話者を取得 + let (choice_model, choice_speaker) = { + let vec: Vec<_> = choice_value.split("||").collect(); + (vec[0], vec[1]) + }; + + let (is_changed_model, before_model) = { + let mut user_data = UserDataMut::from(interaction.user.id).await?; + + // モデルが変更されたかどうか + let is_changed_model = &user_data.model_name != choice_model; + let before_model = user_data.model_name; + + // ユーザーデータを更新 + user_data.model_name = choice_model.to_string(); + user_data.speaker_name = choice_speaker.to_string(); + user_data.style_name = String::default(); + + user_data.update().await?; + + (is_changed_model, before_model) + }; + + // 返答するメッセージを作成 モデルが変更されたなら変更したと通知する + let content = match is_changed_model { + true => format_t!( + "speaker.changed_with_model", + lang, + before_model, + choice_model, + choice_speaker + ), + false => { + format_t!("speaker.changed", lang, choice_speaker) + } + }; + + eq_uilibrium::create_response_msg!(&ctx.http, interaction, content = content, ephemeral = true) + .await?; + Ok(()) +} diff --git a/crates/sonorust/src/components/select_menu/style.rs b/crates/sonorust/src/components/select_menu/style.rs new file mode 100644 index 0000000..7427c97 --- /dev/null +++ b/crates/sonorust/src/components/select_menu/style.rs @@ -0,0 +1,67 @@ +use langrustang::{format_t, lang_t}; +use serenity::all::{ComponentInteraction, ComponentInteractionDataKind, Context}; +use setting_inputter::SettingsJson; +use sonorust_db::UserDataMut; + +use crate::{crate_extensions::SettingsJsonExtension, errors::SonorustError}; + +pub async fn style(ctx: &Context, interaction: &ComponentInteraction) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + // 選択した値の取得 + let choice_value = match &interaction.data.kind { + ComponentInteractionDataKind::StringSelect { values } => &values[0], + _ => { + log::error!(lang_t!("log.fail_get_data")); + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + content = lang_t!("msg.failed.get", lang), + ephemeral = true + ) + .await?; + + return Ok(()); + } + }; + + // 選択したモデルと話者を取得 + let (choice_model, choice_style) = { + let vec: Vec<_> = choice_value.split("||").collect(); + (vec[0], vec[1]) + }; + + let (is_changed_model, before_model) = { + let mut user_data = UserDataMut::from(interaction.user.id).await?; + + // モデルが変更されたかどうか + let is_changed_model = &user_data.model_name != choice_model; + let before_model = user_data.model_name; + + // ユーザーデータを更新 + user_data.model_name = choice_model.to_string(); + user_data.style_name = choice_style.to_string(); + + user_data.update().await?; + + (is_changed_model, before_model) + }; + + // 返答するメッセージを作成 モデルが変更されたなら変更したと通知する + let content = match is_changed_model { + true => format_t!( + "style.changed_with_model", + lang, + before_model, + choice_model, + choice_style + ), + false => { + format_t!("style.changed", lang, choice_style) + } + }; + + eq_uilibrium::create_response_msg!(&ctx.http, interaction, content = content, ephemeral = true) + .await?; + Ok(()) +} diff --git a/crates/sonorust/src/crate_extensions/mod.rs b/crates/sonorust/src/crate_extensions/mod.rs new file mode 100644 index 0000000..67e303f --- /dev/null +++ b/crates/sonorust/src/crate_extensions/mod.rs @@ -0,0 +1,4 @@ +pub mod sbv2_api; +pub mod setting_inputter; + +pub use setting_inputter::SettingsJsonExtension; diff --git a/crates/sonorust/src/crate_extensions/sbv2_api.rs b/crates/sonorust/src/crate_extensions/sbv2_api.rs new file mode 100644 index 0000000..b7be3b3 --- /dev/null +++ b/crates/sonorust/src/crate_extensions/sbv2_api.rs @@ -0,0 +1,235 @@ +use std::{ + collections::{HashMap, VecDeque}, + sync::{LazyLock, RwLock}, + time::Duration, +}; + +use langrustang::lang_t; +use sbv2_api::{Sbv2Client, Sbv2InferParam, Sbv2ModelInfo, ValidModel}; +use serenity::all::{ChannelId, Context, GuildId, UserId}; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; +use songbird::input::Input; +use sonorust_db::{GuildData, UserData}; + +use crate::errors::SonorustError; + +use super::SettingsJsonExtension; + +/// ギルドの読み上げるチャンネルを登録しておく +pub static READ_CHANNELS: LazyLock>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +/// 読み上げるチャンネルの queue +pub static CHANNEL_QUEUES: LazyLock>>> = + LazyLock::new(|| RwLock::new(HashMap::new())); + +pub trait Sbv2ClientExtension { + fn get_valid_model_from_userdata(userdata: &UserData) -> ValidModel; + + async fn infer_from_user(text: &str, userdata: &UserData) -> anyhow::Result>; + + async fn play_on_voice_channel( + ctx: &Context, + guild_id: Option, + channel_id: ChannelId, + user_id: UserId, + play_content: &str, + ) -> Result<(), SonorustError>; +} + +impl Sbv2ClientExtension for Sbv2Client { + fn get_valid_model_from_userdata(userdata: &UserData) -> ValidModel { + let default_model = { + let lock = SETTINGS_JSON.read().unwrap(); + lock.default_model.clone() + }; + + Sbv2ModelInfo::get_valid_model( + &userdata.model_name, + &userdata.speaker_name, + &userdata.style_name, + &default_model, + ) + } + + async fn infer_from_user(text: &str, userdata: &UserData) -> anyhow::Result> { + let client = { + let lock = SETTINGS_JSON.read().unwrap(); + Sbv2Client::from(&lock.host, lock.port) + }; + + let infer_lang = { + let lock = SETTINGS_JSON.read().unwrap(); + lock.infer_lang + }; + + let valid_model = Sbv2Client::get_valid_model_from_userdata(userdata); + + let param = Sbv2InferParam { + model_id: valid_model.model_id, + speaker_id: valid_model.speaker_id, + style_name: valid_model.style_name, + length: userdata.length, + language: infer_lang.to_string(), + }; + + client.infer(text, param).await + } + + async fn play_on_voice_channel( + ctx: &Context, + guild_id: Option, + channel_id: ChannelId, + user_id: UserId, + play_content: &str, + ) -> Result<(), SonorustError> { + // サーバー上でない場合何もしない + let Some(guild_id) = guild_id else { + return Ok(()); + }; + + // 自分自身のメッセージの場合無視する + if user_id == ctx.cache.current_user().id { + return Ok(()); + } + + // 何も再生するメッセージがない場合何もしない + if play_content.is_empty() { + return Ok(()); + } + + let manager = songbird::get(ctx).await.unwrap(); + + let lang = SettingsJson::get_bot_lang(); + + // ボイスチャンネルに参加していないサーバーの場合無視する + let Some(handler_lock) = manager.get(guild_id) else { + return Ok(()); + }; + + // join時に登録した読み上げる対象のチャンネルを取得する + let read_ch = { + let read_channels = READ_CHANNELS.read().unwrap(); + + match read_channels.get(&guild_id) { + Some(ch) => *ch, + None => return Ok(()), + } + }; + + // 読み上げる対象のチャンネルでない場合無視する + if channel_id != read_ch { + return Ok(()); + } + + // そのチャンネルのqueueにメッセージを追加する + { + let mut channel_queues = CHANNEL_QUEUES.write().unwrap(); + let Some(read_ch_queue) = channel_queues.get_mut(&channel_id) else { + log::error!(lang_t!("log.fail_ch_queue")); + return Ok(()); + }; + + read_ch_queue.push_front((play_content.to_string(), user_id)); + + // もし再生待ちが1つだけなら再生に移る + // (下の方ではqueueがなくなるまで繰り返すため) + if read_ch_queue.len() != 1 { + return Ok(()); + } + } + + let infer_lang = { + let lock = SETTINGS_JSON.read().unwrap(); + lock.infer_lang.clone() + }; + + // そのチャンネルのqueueがなくなるまで繰り返す + loop { + // 次に再生する文章とユーザーを取り出す + let (play_content, user_id) = { + // そのチャンネルのqueueを取得 + let mut channel_queues = CHANNEL_QUEUES.write().unwrap(); + let Some(read_ch_queue) = channel_queues.get_mut(&channel_id) else { + log::error!(lang_t!("log.fail_ch_queue")); + return Ok(()); + }; + + // すべてを再生し終えたらreturnして終了する + match read_ch_queue.back() { + Some(s) => s.clone(), + None => return Ok(()), + } + }; + + let Ok(mut userdata) = UserData::from(user_id).await else { + log::error!(lang_t!("log.fail_update_guilddata")); + return Ok(()); + }; + + let Ok(guilddata) = GuildData::from(guild_id).await else { + log::error!(lang_t!("log.fail_get_userdata")); + return Ok(()); + }; + + // オプションがオンになっていて一定の文字数より多い場合、素早く読む + let fastread_border = { + use setting_inputter::settings_json::SettingLang::*; + + match infer_lang { + Ja => 30, + En => 80, + } + }; + + if guilddata.options.is_if_long_fastread + && play_content.chars().count() >= fastread_border + { + userdata.length = 0.5; + } + + // 音声を生成 + // API が起動していなく、音声を生成できなかった場合 VC から退出する + let Ok(voice_data) = Sbv2Client::infer_from_user(&play_content, &userdata).await else { + if let Err(err) = manager.remove(guild_id).await { + log::error!("{}: {err}", lang_t!("log.fail_leave_vc")) + } + channel_id + .say(&ctx.http, lang_t!("msg.failed.infer", lang)) + .await?; + return Ok(()); + }; + + // 再生時間を求める + // ビット数 = Vecの数 * 8 + // 1秒あたりの情報量 = 44.1 kHz * 16 bit + let voice_playtime = (voice_data.len() * 8) as f64 / (44100.0 * 16.0); + + // 音声を VC で作成 + let input = Input::from(voice_data); + { + let mut handler = handler_lock.lock().await; + + let track_handle = handler.play_input(input); + if let Err(err) = track_handle.set_volume(0.1) { + log::error!("{}: {err}", lang_t!("log.fail_adj_vol")) + } + } + + // その音声の再生時間だけスリープする + let duration = Duration::from_secs_f64(voice_playtime); + tokio::time::sleep(duration).await; + + // 再生した音声を queue から削除する + { + let mut channel_queues = CHANNEL_QUEUES.write().unwrap(); + let Some(read_ch_queue) = channel_queues.get_mut(&channel_id) else { + log::error!(lang_t!("log.fail_get_userdata")); + return Ok(()); + }; + + read_ch_queue.pop_back(); + } + } + } +} diff --git a/crates/sonorust/src/crate_extensions/setting_inputter.rs b/crates/sonorust/src/crate_extensions/setting_inputter.rs new file mode 100644 index 0000000..2001dbf --- /dev/null +++ b/crates/sonorust/src/crate_extensions/setting_inputter.rs @@ -0,0 +1,28 @@ +use std::sync::LazyLock; + +use crate::_langrustang_autogen::Lang; +use setting_inputter::settings_json::SettingLang; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; + +// キャッシュしておく (再起動しないと更新されない) +static LANG_CACHE: LazyLock = LazyLock::new(|| { + let lang = { + let lock = SETTINGS_JSON.read().unwrap(); + lock.bot_lang + }; + + match lang { + SettingLang::Ja => Lang::Ja, + SettingLang::En => Lang::En, + } +}); + +pub trait SettingsJsonExtension { + fn get_bot_lang() -> Lang; +} + +impl SettingsJsonExtension for SettingsJson { + fn get_bot_lang() -> Lang { + *LANG_CACHE + } +} diff --git a/crates/sonorust/src/errors.rs b/crates/sonorust/src/errors.rs new file mode 100644 index 0000000..2ef6c21 --- /dev/null +++ b/crates/sonorust/src/errors.rs @@ -0,0 +1,141 @@ +use std::fmt::Display; + +use langrustang::lang_t; +use serenity::all::{ChannelId, Colour, Context, CreateEmbed, GuildId, Interaction}; +use setting_inputter::SettingsJson; +use sonorust_db::SonorustDBError; + +use crate::crate_extensions::SettingsJsonExtension; + +#[derive(Debug)] +pub enum SonorustError { + SonorustDBError(SonorustDBError), + SerenityError(serenity::Error), + UseOnNotGuild, +} + +impl SonorustError { + /// エラーメッセージを discord に送信する + pub async fn send_err_msg( + &self, + ctx: &Context, + channel_id: ChannelId, + ) -> Result<(), serenity::Error> { + let embed = self.create_embed(); + + eq_uilibrium::send_msg!(&ctx.http, channel_id, embed = embed).await?; + Ok(()) + } + + pub async fn send_err_responce( + &self, + ctx: &Context, + interaction: &Interaction, + ) -> Result<(), serenity::Error> { + let embed = self.create_embed(); + + match interaction { + Interaction::Command(command_interaction) => { + eq_uilibrium::create_response_msg!(&ctx.http, command_interaction, embed = embed) + .await?; + } + Interaction::Component(component_interaction) => { + eq_uilibrium::create_response_msg!(&ctx.http, component_interaction, embed = embed) + .await?; + } + Interaction::Modal(modal_interaction) => { + eq_uilibrium::create_response_msg!(&ctx.http, modal_interaction, embed = embed) + .await?; + } + Interaction::Ping(_) => { + log::error!("Can't respond ping interaction: {}", self); + } + Interaction::Autocomplete(command_interaction) => { + eq_uilibrium::create_response_msg!(&ctx.http, command_interaction, embed = embed) + .await?; + } + + _ => log::error!("Can't respond Error message: {}", self), + } + + Ok(()) + } + + fn create_embed(&self) -> CreateEmbed { + let lang = SettingsJson::get_bot_lang(); + + let (title, description) = match self { + SonorustError::SerenityError(error) => (lang_t!("msg.error", lang), error.to_string()), + SonorustError::UseOnNotGuild => ( + lang_t!("msg.only_use_guild_1", lang), + lang_t!("msg.only_use_guild_2", lang).to_string(), + ), + + SonorustError::SonorustDBError(error) => ( + lang_t!("msg.error", lang), + match error { + SonorustDBError::InitDatabase(_) => lang_t!("msg.failed.update", lang), + SonorustDBError::GetUserData(_) => lang_t!("msg.failed.get", lang), + SonorustDBError::UpdateUserData(_) => lang_t!("msg.failed.update", lang), + SonorustDBError::GetGuildData(_) => lang_t!("msg.failed.get", lang), + SonorustDBError::UpdateGuildData(_) => lang_t!("msg.failed.update", lang), + SonorustDBError::Unknown(_) => lang_t!("msg.error", lang), + } + .to_string(), + ), + }; + + CreateEmbed::new() + .title(title) + .description(description) + .colour(Colour::from_rgb(255, 0, 0)) + } +} + +impl Display for SonorustError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use SonorustError::*; + + match self { + SonorustDBError(sonorust_dberror) => write!(f, "{}", sonorust_dberror), + SerenityError(error) => write!(f, "{}", error), + UseOnNotGuild => write!(f, "Guild Id is None"), + } + } +} + +impl std::error::Error for SonorustError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + use SonorustError::*; + + match self { + SonorustDBError(sonorust_dberror) => Some(sonorust_dberror), + SerenityError(error) => Some(error), + UseOnNotGuild => None, + } + } +} + +// 各エラー型からの変換 +impl From for SonorustError { + fn from(value: SonorustDBError) -> Self { + SonorustError::SonorustDBError(value) + } +} + +impl From for SonorustError { + fn from(value: serenity::Error) -> Self { + SonorustError::SerenityError(value) + } +} + +// GuildId が None だった時に?でリターンする用 +pub trait NoneToSonorustError { + fn ok_or_sonorust_err(self) -> Result; +} + +impl NoneToSonorustError for Option { + fn ok_or_sonorust_err(self) -> Result { + self.ok_or(SonorustError::UseOnNotGuild) + } +} diff --git a/crates/sonorust/src/main.rs b/crates/sonorust/src/main.rs new file mode 100644 index 0000000..63cd33d --- /dev/null +++ b/crates/sonorust/src/main.rs @@ -0,0 +1,162 @@ +langrustang::i18n!("./lang/bot_lang.yaml"); + +mod commands; +mod components; +mod crate_extensions; +mod errors; +mod registers; + +use std::time::Duration; + +use engtokana::EngToKana; +use sbv2_api::Sbv2Client; +use serenity::all::GatewayError::DisallowedGatewayIntents; +use serenity::{ + all::{Context, EventHandler, GatewayIntents, Interaction, Message, Ready, VoiceState}, + async_trait, Client, +}; +use setting_inputter::settings_json::{SettingsJson, SETTINGS_JSON}; +use songbird::SerenityInit as _; +use tokio::runtime::Runtime; + +pub struct Handler; + +#[async_trait] +impl EventHandler for Handler { + async fn ready(&self, ctx: Context, ready: Ready) { + registers::ready(&ctx, &ready).await; + } + + async fn message(&self, ctx: Context, msg: Message) { + if let Err(err) = registers::message(&ctx, &msg).await { + if let Err(err) = err.send_err_msg(&ctx, msg.channel_id).await { + log::error!("Can't respond message: {}", err); + } + } + } + + async fn interaction_create(&self, ctx: Context, interaction: Interaction) { + match &interaction { + Interaction::Command(inter) => { + if let Err(err) = registers::slash_commands(&ctx, inter).await { + if let Err(err) = err.send_err_responce(&ctx, &interaction).await { + log::error!("Can't respond interaction: {}", err); + } + } + } + + Interaction::Component(inter) => { + if let Err(err) = registers::components(&ctx, inter).await { + if let Err(err) = err.send_err_responce(&ctx, &interaction).await { + log::error!("Can't respond interaction: {}", err); + } + } + } + + _ => (), + } + } + + async fn voice_state_update(&self, ctx: Context, old: Option, new: VoiceState) { + if let Err(sonorust_err) = registers::voice_state_update(ctx, old, new).await { + log::error!("Can't respond voice_state_update: {}", sonorust_err); + } + } +} + +pub async fn bot_start() { + // json ファイルの初期設定 + { + let settings_json = SettingsJson::new(); + let mut lock = SETTINGS_JSON.write().unwrap(); + *lock = settings_json; + } + + // sbv2 の準備 + let (sbv2_client, sbv2_path) = { + let lock = SETTINGS_JSON.read().unwrap(); + ( + Sbv2Client::from(&lock.host, lock.port), + lock.sbv2_path.clone(), + ) + }; + + /// 起動できなかった場合に待機してから終了する処理 + async fn exit_program(text: &str) { + log::error!("{}", text); + tokio::time::sleep(Duration::from_secs(30)).await; + + std::process::exit(1); + } + + // sbv2の起動 + if !sbv2_client.is_api_activation().await { + if let Some(path) = sbv2_path { + if cfg!(target_os = "windows") { + log::info!("Waiting for SBV2 API to start..."); + + if let Err(_) = sbv2_client.launch_api_win(&path).await { + exit_program("The API could not be started. Exit the program.").await; + } + + log::info!("The API has been started."); + } + } + } + + if let Err(_) = sbv2_client.update_modelinfo().await { + exit_program("Failed to Connect SBV2 API.").await; + } + + // カタカナ読み辞書の初期化 + if let Err(_) = EngToKana::download_init_dic().await { + exit_program("Failed to download the kana reading dictionary. Exit the program.").await; + }; + + // BOTのclient作成 + let intents = GatewayIntents::all(); + + // ログインできなかった場合トークンをもう一度入力してもらいログインを試す + loop { + let token = setting_inputter::get_or_set_token() + .await + .expect("Can't open file"); + + let mut client = Client::builder(token, intents) + .event_handler(Handler) + .register_songbird() + .await + .expect("Can't create client"); + + // Bot にログイン + let result = client.start().await; + + match result { + // Intents が足りてなかった場合 + Err(serenity::Error::Gateway(DisallowedGatewayIntents)) => { + exit_program( + "Missing intent, please change the settings in the Discord Developer Portal. (https://discord.com/developers/applications)" + ).await; + } + + // ログインできなかった場合 + Err(_) => { + log::info!("Login failed. Input Discord Bot Token."); + setting_inputter::input_token() + .await + .expect("Can't open file"); + + continue; + } + + Ok(_) => break, + } + } +} + +fn main() { + sonorust_logger::setup_logger(); + + let runtime = Runtime::new().unwrap(); + runtime.block_on(async { bot_start().await }); +} diff --git a/crates/sonorust/src/registers/components.rs b/crates/sonorust/src/registers/components.rs new file mode 100644 index 0000000..d514e81 --- /dev/null +++ b/crates/sonorust/src/registers/components.rs @@ -0,0 +1,64 @@ +use langrustang::lang_t; +use serenity::all::{ComponentInteraction, Context}; + +use crate::{components, errors::SonorustError}; + +pub async fn components( + ctx: &Context, + interaction: &ComponentInteraction, +) -> Result<(), SonorustError> { + let custom_id = &interaction.data.custom_id; + + log::debug!( + "Component used: {custom_id} {{ Name: {}, ID: {} }}", + interaction.user.name, + interaction.user.id, + ); + + match custom_id.as_str() { + lang_t!("customid.select.model") => { + components::select_menu::model(ctx, interaction).await? + } + lang_t!("customid.select.speaker") => { + components::select_menu::speaker(ctx, interaction).await? + } + lang_t!("customid.select.style") => { + components::select_menu::style(ctx, interaction).await? + } + + lang_t!("customid.page.model.forward") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + lang_t!("customid.page.model.back") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + lang_t!("customid.page.speaker.forward") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + lang_t!("customid.page.speaker.back") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + lang_t!("customid.page.style.forward") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + lang_t!("customid.page.style.back") => { + components::button::move_page(ctx, interaction, custom_id).await? + } + + lang_t!("customid.change_server_settings") => { + components::select_menu::server(ctx, interaction).await? + } + + lang_t!("customid.dict.add") => components::button::dict_add(ctx, interaction).await?, + lang_t!("customid.dict.remove") => { + components::button::dict_remove(ctx, interaction).await? + } + + _ => { + log::error!(lang_t!("log.not_implemented_customid")); + return Ok(()); + } + } + + Ok(()) +} diff --git a/crates/sonorust/src/registers/message.rs b/crates/sonorust/src/registers/message.rs new file mode 100644 index 0000000..e7a57b3 --- /dev/null +++ b/crates/sonorust/src/registers/message.rs @@ -0,0 +1,423 @@ +use std::{collections::HashMap, time}; + +use engtokana::EngToKana; +use langrustang::{format_t, lang_t}; +use regex::Regex; +use sbv2_api::Sbv2Client; +use serenity::all::{Context, CreateMessage, EditMessage, Message}; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; +use sonorust_db::GuildData; + +use crate::{ + commands, + crate_extensions::{ + sbv2_api::{Sbv2ClientExtension, READ_CHANNELS}, + SettingsJsonExtension, + }, + errors::SonorustError, +}; + +pub async fn message(ctx: &Context, msg: &Message) -> Result<(), SonorustError> { + // 実際の動作は commands フォルダ + + // メッセージを送信したのがBOTだった場合無視 + if msg.author.bot { + return Ok(()); + } + + // prefix を取得 + let prefix = { + let settings_json = SETTINGS_JSON.read().unwrap(); + settings_json.prefix.clone() + }; + + // prefix から始まっているかによって処理を変える + match msg.content.starts_with(&prefix) { + true => command_processing(ctx, msg, &prefix).await, + false => other_processing(ctx, msg).await, + } +} + +/// メッセージの内容がコマンドだった場合の処理 +async fn command_processing( + ctx: &Context, + msg: &Message, + prefix: &str, +) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + + // メッセージからプレフィックスを除いたものを取得 + let msg_suffix = &msg.content[prefix.len()..]; + + // コマンドが使用されたときのデバッグログ + let debug_log = || { + log::debug!( + "MessageCommand used: /{msg_suffix} {{ Name: {}, ID: {} }}", + msg.author.name, + msg.author.id, + ) + }; + + match msg_suffix { + "ping" => { + debug_log(); + + // pong を送信 + let embed = commands::ping::measuring_embed(); + let message = CreateMessage::new().embed(embed); + + let now = time::Instant::now(); + let mut send_msg = msg.channel_id.send_message(&ctx.http, message).await?; + let elapsed = now.elapsed(); + + // description を計測した時間に書き換え + let embed = commands::ping::measured_embed(elapsed); + let edit_msg = EditMessage::new().embed(embed); + send_msg.edit(&ctx.http, edit_msg).await?; + } + + "help" => { + debug_log(); + + let embed = commands::help(ctx).await; + eq_uilibrium::send_msg!(&ctx.http, msg.channel_id, embed = embed).await?; + } + + "now" => { + debug_log(); + + let embed = commands::now(&msg.author).await?; + eq_uilibrium::send_msg!(&ctx.http, msg.channel_id, embed = embed).await?; + } + + "join" => { + debug_log(); + + let result = commands::join(ctx, msg.guild_id, msg.channel_id, msg.author.id).await; + + let text = match result { + Ok(str) => str, + Err(str) => str, + }; + + let help_embed = commands::help(ctx).await; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + content = text, + embed = help_embed + ) + .await?; + + // すでにボイスチャンネルに参加していた場合などは返す + if let Err(_) = result { + return Ok(()); + }; + + Sbv2Client::play_on_voice_channel( + ctx, + msg.guild_id, + msg.channel_id, + msg.author.id, + lang_t!("join.connected", lang), + ) + .await?; + } + + "leave" => { + debug_log(); + + let text = commands::leave(ctx, msg.guild_id).await; + msg.channel_id.say(&ctx.http, text).await?; + } + + "model" => { + debug_log(); + + let (embed, components) = commands::model().await; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + embed = embed, + components = components + ) + .await?; + } + + "speaker" => { + debug_log(); + + let (embed, components) = commands::speaker(msg.author.id).await?; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + embed = embed, + components = components + ) + .await?; + } + + "style" => { + debug_log(); + + let (embed, components) = commands::style(msg.author.id).await?; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + embed = embed, + components = components + ) + .await?; + } + + "server" => { + debug_log(); + + let (embed, components) = commands::server(ctx, msg.guild_id, msg.author.id).await?; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + embed = embed, + components = components + ) + .await?; + } + + "dict" => { + debug_log(); + + let (embed, components) = commands::dict(ctx, msg.guild_id).await?; + eq_uilibrium::send_msg!( + &ctx.http, + msg.channel_id, + embed = embed, + components = components + ) + .await?; + } + + "reload" => { + debug_log(); + + let content = match commands::reload(msg.author.id).await { + Ok(text) => text, + Err(_) => { + log::error!(lang_t!("log.fail_conn_api")); + lang_t!("msg.failed.update", lang) + } + }; + + msg.channel_id.say(&ctx.http, content).await?; + } + + _ => (), + } + + if msg_suffix.starts_with("length") { + debug_log(); + + let args: Vec<_> = msg_suffix.split_whitespace().collect(); + + // 数字部分を取得 + let Some(length) = args.get(1).map(|i| *i) else { + msg.channel_id + .say(&ctx.http, format_t!("length.usage", lang, prefix)) + .await?; + return Ok(()); + }; + + // 数字に変換できなければ返す + let Ok(length): Result = length.parse() else { + msg.channel_id + .say(&ctx.http, lang_t!("length.not_num", lang)) + .await?; + return Ok(()); + }; + + // ユーザーデータを変更してメッセージを送信 + let content = commands::length(msg.author.id, length).await?; + msg.channel_id.say(&ctx.http, content).await?; + } + + if msg_suffix.starts_with("wav") { + debug_log(); + + let args: Vec<_> = msg_suffix.split_whitespace().collect(); + + // 生成部分を取得 + let Some(content) = args.get(1).map(|i| *i) else { + msg.channel_id + .say(&ctx.http, format_t!("wav.usage", lang, prefix)) + .await?; + return Ok(()); + }; + + match commands::wav(msg.author.id, content).await? { + (None, Some(s)) => { + eq_uilibrium::send_msg!(&ctx.http, msg.channel_id, content = s).await?; + } + (Some(attachment), None) => { + eq_uilibrium::send_msg!(&ctx.http, msg.channel_id, add_file = attachment).await?; + } + + _ => (), + }; + } + + Ok(()) +} + +/// メッセージの内容がコマンド以外だった場合の処理 +async fn other_processing(ctx: &Context, msg: &Message) -> Result<(), SonorustError> { + // セミコロンから始まっていた場合無視 + if msg.content.starts_with(";") { + return Ok(()); + } + + // ギルド上でない場合無視 + let Some(guild_id) = msg.guild_id else { + return Ok(()); + }; + + // 読み上げるチャンネルか確認 + let read_target_ch = { + let read_channels = READ_CHANNELS.read().unwrap(); + read_channels.get(&guild_id).map(|i| *i) + }; + + if read_target_ch != Some(msg.channel_id) { + return Ok(()); + } + + // ギルドの設定を取得 + let guilddata = GuildData::from(guild_id).await?; + + let lang = SettingsJson::get_bot_lang(); + + // 読み上げ用に文字を置換する + let mut text_replace = TextReplace::new(&msg.content); + + text_replace.remove_codeblock(); + text_replace.remove_discord_obj(); + text_replace.remove_url(); + + text_replace.replace_from_guilddict(&guilddata); + + // 日本語の時のみ英語を日本語読みに変換 + { + use crate::_langrustang_autogen::Lang::*; + + match lang { + Ja => text_replace.eng_to_kana(), + _ => (), + } + } + + text_replace.remove_emoji(); + + let replaced_text = text_replace.as_string(); + + let read_limit = { + let settings_json = SETTINGS_JSON.read().unwrap(); + settings_json.read_limit + }; + + // read_limit よりも長い場合はその長さに制限する + let content = match replaced_text.char_indices().nth(read_limit as _) { + Some((idx, _)) => format_t!("msg.omitted", lang, &msg.content[..idx]), + None => replaced_text, + }; + + let lang = SettingsJson::get_bot_lang(); + + // 設定で ON になっていて添付ファイルがあるなら添付ファイルがあることを知らせる + if !msg.attachments.is_empty() && guilddata.options.is_notice_attachment { + Sbv2Client::play_on_voice_channel( + ctx, + msg.guild_id, + msg.channel_id, + msg.author.id, + lang_t!("msg.attachments", lang), + ) + .await?; + } + + Sbv2Client::play_on_voice_channel(ctx, msg.guild_id, msg.channel_id, msg.author.id, &content) + .await?; + + Ok(()) +} + +// 読み方を編集する用 (サーバー辞書など) +#[derive(Debug, Clone)] +pub struct TextReplace { + text: String, +} + +impl TextReplace { + pub fn new(text: S) -> Self + where + S: Into, + { + Self { text: text.into() } + } + + pub fn as_string(self) -> String { + self.text + } + + pub fn remove_codeblock(&mut self) { + // ``` が含まれていた場合全体をコードブロックと読む + if self.text.contains("```") { + self.text = "コードブロック".to_string(); + return; + } + + let re = Regex::new(r"`.*?`").unwrap(); + self.text = re.replace_all(&self.text, "コード").to_string() + } + + pub fn remove_url(&mut self) { + let re = Regex::new(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+").unwrap(); + self.text = re.replace_all(&self.text, "URL").to_string() + } + + /// チャンネルやメンション、カスタム絵文字などの置換 + pub fn remove_discord_obj(&mut self) { + let re = Regex::new(r"<.*?>").unwrap(); + self.text = re.replace_all(&self.text, "").to_string() + } + + pub fn remove_emoji(&mut self) { + let re = Regex::new(r"[^\p{L}\p{N}\p{Pd}\p{Sm}\p{Sc}]").unwrap(); + self.text = re.replace_all(&self.text, "").to_string() + } + + /// 指定したサーバー辞書をもとに置換する + pub fn replace_from_guilddict(&mut self, guilddata: &GuildData) { + let map = &guilddata.dict; + + self.replace_from_hashmap(map); + } + + /// HashMap をもとに HashMap の Key を Value に置換する + pub fn replace_from_hashmap(&mut self, map: &HashMap) { + let mut replace_texts = HashMap::new(); + + for (i, (before, after)) in map.iter().enumerate() { + let mark = format!("{{|{}|}}", i); + + self.text = self.text.replace(before, &mark).to_string(); + replace_texts.insert(mark, after); + } + + for (before, after) in replace_texts { + self.text = self.text.replace(&before, after) + } + } + + /// 英語をカタカナ読みに変換する + pub fn eng_to_kana(&mut self) { + self.text = EngToKana::convert_all(&self.text); + } +} diff --git a/crates/sonorust/src/registers/mod.rs b/crates/sonorust/src/registers/mod.rs new file mode 100644 index 0000000..c8be3a6 --- /dev/null +++ b/crates/sonorust/src/registers/mod.rs @@ -0,0 +1,13 @@ +mod components; +mod message; +mod ready; +mod slash_commands; +mod voice_state_update; + +pub use components::components; +pub use message::message; +pub use ready::ready; +pub use slash_commands::slash_commands; +pub use voice_state_update::voice_state_update; + +pub use ready::APP_OWNER_ID; diff --git a/crates/sonorust/src/registers/ready.rs b/crates/sonorust/src/registers/ready.rs new file mode 100644 index 0000000..e9ca6f2 --- /dev/null +++ b/crates/sonorust/src/registers/ready.rs @@ -0,0 +1,44 @@ +use std::sync::{LazyLock, RwLock}; + +use serenity::all::{Command, Context, Ready, UserId}; + +use crate::registers::slash_commands; + +/// BOT の所有者のユーザーID +pub static APP_OWNER_ID: LazyLock>> = LazyLock::new(|| RwLock::new(None)); + +pub async fn ready(ctx: &Context, ready: &Ready) { + log::info!("{} is connected!", ready.user.name); + + // BOT の所有者のユーザーIDを変数に保存 + let app_owner = ctx + .http + .get_current_application_info() + .await + .and_then(|i| Ok(i.owner)); + + if let Ok(Some(owner)) = app_owner { + let mut app_owner_id = APP_OWNER_ID.write().unwrap(); + *app_owner_id = Some(owner.id); + log::debug!("Updated app_owner_id: {:?}", app_owner_id); + } + + // テスト用: 環境変数 (IS_SYNC_SLASH) が false なら同期しない + let is_sync_slash: bool = match std::env::var("IS_SYNC_SLASH").map(|i| i.parse()) { + Ok(Ok(b)) => b, + _ => true, + }; + + if !is_sync_slash { + return; + } + + // スラッシュコマンドの登録 + log::info!("Registering SlashCommands..."); + + let commands = slash_commands::registers(); + match Command::set_global_commands(&ctx.http, commands).await { + Ok(_) => log::info!("Slash command has been registered."), + Err(_) => log::error!("Failed to register slash command."), + } +} diff --git a/crates/sonorust/src/registers/slash_commands.rs b/crates/sonorust/src/registers/slash_commands.rs new file mode 100644 index 0000000..474c62f --- /dev/null +++ b/crates/sonorust/src/registers/slash_commands.rs @@ -0,0 +1,278 @@ +use std::time::Instant; + +use langrustang::lang_t; +use sbv2_api::Sbv2Client; +use serenity::all::{ + CommandInteraction, Context, CreateCommand, CreateInteractionResponse, + CreateInteractionResponseFollowup, CreateInteractionResponseMessage, EditMessage, + ResolvedOption, ResolvedValue, +}; +use setting_inputter::SettingsJson; + +use crate::{ + commands, + crate_extensions::{sbv2_api::Sbv2ClientExtension as _, SettingsJsonExtension as _}, + errors::SonorustError, +}; + +pub async fn slash_commands( + ctx: &Context, + interaction: &CommandInteraction, +) -> Result<(), SonorustError> { + let lang = SettingsJson::get_bot_lang(); + let command_name = interaction.data.name.as_str(); + + // コマンドが使用されたときのデバッグログ + let debug_log = || { + log::debug!( + "SlashCommand used: /{command_name} (Name: {}, ID: {})", + interaction.user.name, + interaction.user.id, + ) + }; + + match command_name { + "ping" => { + debug_log(); + + // pong を送信 + let embed = commands::ping::measuring_embed(); + let message = CreateInteractionResponseMessage::new().embed(embed); + let builder = CreateInteractionResponse::Message(message); + + let now = Instant::now(); + interaction.create_response(&ctx.http, builder).await?; + let elapsed = now.elapsed(); + + let mut send_msg = interaction.get_response(&ctx.http).await?; + + // description を計測した時間に書き換え + let embed = commands::ping::measured_embed(elapsed); + let edit_msg = EditMessage::new().embed(embed); + + send_msg.edit(&ctx.http, edit_msg).await?; + } + + "help" => { + debug_log(); + + let embed = commands::help(ctx).await; + eq_uilibrium::create_response_msg!(&ctx.http, interaction, embed = embed).await?; + } + + "now" => { + debug_log(); + + let embed = commands::now(&interaction.user).await?; + eq_uilibrium::create_response_msg!(&ctx.http, interaction, embed = embed).await?; + } + + "join" => { + debug_log(); + + // Defer を送信 + let msg = CreateInteractionResponseMessage::new(); + let builder = CreateInteractionResponse::Defer(msg); + interaction.create_response(&ctx.http, builder).await?; + + let result = commands::join( + ctx, + interaction.guild_id, + interaction.channel_id, + interaction.user.id, + ) + .await; + + let text = match result { + Ok(s) => s, + Err(s) => s, + }; + + let help_embed = commands::help(ctx).await; + let builder = CreateInteractionResponseFollowup::new() + .content(text) + .embed(help_embed); + + interaction.create_followup(&ctx.http, builder).await?; + + // すでにボイスチャンネルに参加していた場合などは返す + let Ok(_) = result else { + return Ok(()); + }; + + // 接続音声を再生 + Sbv2Client::play_on_voice_channel( + ctx, + interaction.guild_id, + interaction.channel_id, + interaction.user.id, + lang_t!("join.connected", lang), + ) + .await?; + } + + "leave" => { + debug_log(); + + let content = commands::leave(ctx, interaction.guild_id).await; + eq_uilibrium::create_response_msg!(&ctx.http, interaction, content = content).await?; + } + + "model" => { + debug_log(); + + let (embed, components) = commands::model().await; + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = components, + ) + .await?; + } + + "speaker" => { + debug_log(); + + let (embed, components) = commands::speaker(interaction.user.id).await?; + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = components, + ) + .await?; + } + + "style" => { + debug_log(); + + let (embed, components) = commands::style(interaction.user.id).await?; + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = components, + ) + .await?; + } + + "server" => { + debug_log(); + + let (embed, components) = + commands::server(ctx, interaction.guild_id, interaction.user.id).await?; + + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = components, + ) + .await?; + } + + "dict" => { + debug_log(); + + let (embed, components) = commands::dict(ctx, interaction.guild_id).await?; + eq_uilibrium::create_response_msg!( + &ctx.http, + interaction, + embed = embed, + components = components, + ) + .await?; + } + + "reload" => { + debug_log(); + + let content = match commands::reload(interaction.user.id).await { + Ok(text) => text, + Err(_) => { + log::error!(lang_t!("log.fail_conn_api")); + lang_t!("msg.failed.update", lang) + } + }; + + eq_uilibrium::create_response_msg!(&ctx.http, interaction, content = content).await?; + } + + "length" => { + debug_log(); + + // スラッシュコマンドの引数を取得 + let command_args = &interaction.data.options(); + let length: f64 = match command_args.get(0) { + Some(ResolvedOption { + value: ResolvedValue::Number(num), + .. + }) => *num as _, + + _ => 1.0, + }; + + let content = commands::length(interaction.user.id, length).await?; + eq_uilibrium::create_response_msg!(&ctx.http, interaction, content = content).await?; + } + + "wav" => { + debug_log(); + + // Defer を送信 + let msg = CreateInteractionResponseMessage::new(); + let builder = CreateInteractionResponse::Defer(msg); + interaction.create_response(&ctx.http, builder).await?; + + // スラッシュコマンドの引数を取得 + let command_args = &interaction.data.options(); + let content = match command_args.get(0) { + Some(ResolvedOption { + value: ResolvedValue::String(content), + .. + }) => content.to_string(), + + _ => String::default(), + }; + + let builder = match commands::wav(interaction.user.id, &content).await? { + (None, Some(s)) => CreateInteractionResponseFollowup::new().content(s), + (Some(attachment), None) => { + CreateInteractionResponseFollowup::new().add_file(attachment) + } + _ => return Ok(()), + }; + + interaction.create_followup(&ctx.http, builder).await?; + } + + _ => { + log::error!( + "{}: {}", + lang_t!("log.not_implemented_command"), + command_name + ); + } + }; + + return Ok(()); +} + +pub fn registers() -> Vec { + vec![ + commands::dict::create_command(), + commands::help::create_command(), + commands::join::create_command(), + commands::leave::create_command(), + commands::length::create_command(), + commands::model::create_command(), + commands::now::create_command(), + commands::ping::create_command(), + commands::reload::create_command(), + commands::server::create_command(), + commands::speaker::create_command(), + commands::style::create_command(), + commands::wav::create_command(), + ] +} diff --git a/crates/sonorust/src/registers/voice_state_update.rs b/crates/sonorust/src/registers/voice_state_update.rs new file mode 100644 index 0000000..a2cdabb --- /dev/null +++ b/crates/sonorust/src/registers/voice_state_update.rs @@ -0,0 +1,296 @@ +use std::collections::{HashMap, VecDeque}; + +use langrustang::{format_t, lang_t}; +use sbv2_api::Sbv2Client; +use serenity::all::{ChannelId, Context, GuildId, UserId, VoiceState}; +use setting_inputter::{settings_json::SETTINGS_JSON, SettingsJson}; +use sonorust_db::GuildData; + +use crate::{ + crate_extensions::{ + sbv2_api::{Sbv2ClientExtension, CHANNEL_QUEUES, READ_CHANNELS}, + SettingsJsonExtension, + }, + errors::SonorustError, +}; + +use super::message::TextReplace; + +#[derive(Debug, Clone, Copy)] +enum UserAction { + Entrance, + Exit, +} + +pub async fn voice_state_update( + ctx: Context, + old: Option, + new: VoiceState, +) -> Result<(), SonorustError> { + auto_join(&ctx, new.guild_id, new.user_id).await?; + + let fn_log_play = |channel_id, user_action| { + entrance_exit_log_play(&ctx, new.guild_id, channel_id, new.user_id, user_action) + }; + + if let Some(old) = old { + if new.channel_id != old.channel_id { + if let Some(channel_id) = old.channel_id { + fn_log_play(channel_id, UserAction::Exit).await?; + } + if let Some(channel_id) = new.channel_id { + fn_log_play(channel_id, UserAction::Entrance).await?; + } + + auto_exit(&ctx, new.guild_id).await; + } + } else { + if let Some(channel_id) = new.channel_id { + fn_log_play(channel_id, UserAction::Entrance).await?; + } + } + + Ok(()) +} + +/// もし自分だけになったら自動退出する処理 +async fn auto_exit(ctx: &Context, guild_id: Option) { + // サーバー内ではない場合何もしない + let Some(guild_id) = guild_id else { + return; + }; + + let manager = songbird::get(ctx).await.unwrap(); + + // 自分が vc に参加していないなら何もしない + if let None = manager.get(guild_id) { + return; + } + + let in_vc_users = { + let guild = match guild_id.to_guild_cached(&ctx.cache) { + Some(guild) => guild, + None => return, + }; + + let voice_states: &HashMap = &guild.voice_states; + + // 自分自身がいるボイスチャンネルの id を取得 + let self_in_vc = match voice_states.get(&ctx.cache.current_user().id) { + Some(ch) => ch.channel_id, + None => return, + }; + + // そのチャンネル id にいるユーザーリストを取得 + voice_states + .iter() + .filter(|(_, v)| v.channel_id == self_in_vc) // 同じVCにいるユーザーだけ取得 + .map(|(k, _)| *k) // UserId だけ取得 + .collect::>() + }; + + // 自分だけではないなら何もしない + if in_vc_users.len() != 1 { + return; + } + + // ボイスチャンネルから切断する + let _ = manager.remove(guild_id).await; + + log::debug!("Auto exited: {{ GuildID: {} }}", guild_id); +} + +/// サーバー設定で自動参加が ON になっているなら自動参加する処理 +async fn auto_join( + ctx: &Context, + guild_id: Option, + user_id: UserId, +) -> Result<(), SonorustError> { + // サーバー内ではない場合何もしない + let Some(guild_id) = guild_id else { + return Ok(()); + }; + + let client = { + let lock = SETTINGS_JSON.read().unwrap(); + Sbv2Client::from(&lock.host, lock.port) + }; + + // Api が起動していない場合何もしない + if !client.is_api_activation().await { + return Ok(()); + } + + // もしそのサーバーのVCにすでにいる場合何もしない + let manager = songbird::get(ctx).await.unwrap().clone(); + if let Some(_) = manager.get(guild_id) { + return Ok(()); + } + + // そのサーバーで VC 自動参加が ON になっていないなら何もしない + let guild_data = GuildData::from(guild_id).await?; + + if !guild_data.options.is_auto_join { + return Ok(()); + } + + // ユーザーがいるVCを取得する処理 + let user_vchannel = { + let guild = guild_id.to_guild_cached(&ctx.cache).unwrap(); + guild.voice_states.get(&user_id).and_then(|v| v.channel_id) + }; + + // 使用したユーザーがVCに参加していない場合返す + let connect_channel = match user_vchannel { + Some(user_vc) => user_vc, + None => return Ok(()), + }; + + // もしVCに参加できなかったら返す + if let Err(_) = manager.join(guild_id, connect_channel).await { + log::error!(lang_t!("log.fail_join_vc")); + return Ok(()); + } + + // サーバーIDと読み上げるチャンネルIDのペアを登録 + { + let mut read_channels = READ_CHANNELS.write().unwrap(); + read_channels.insert(guild_id, connect_channel); + } + + // 読み上げ queue を初期化 + { + let mut channel_queues = CHANNEL_QUEUES.write().unwrap(); + channel_queues.insert(connect_channel, VecDeque::new()); + } + + // メッセージを送信して VC で再生 + let lang = SettingsJson::get_bot_lang(); + + let _ = connect_channel + .say(&ctx.http, lang_t!("join.connected", lang)) + .await; + + let _ = Sbv2Client::play_on_voice_channel( + ctx, + Some(guild_id), + connect_channel, + user_id, + lang_t!("join.connected", lang), + ) + .await; + + log::debug!( + "Auto joined: {{ GuildID: {}, ChannelID: {} }}", + guild_id, + connect_channel + ); + + Ok(()) +} + +/// 入退出のログを残す処理 +async fn entrance_exit_log_play( + ctx: &Context, + guild_id: Option, + channel_id: ChannelId, + user_id: UserId, + user_action: UserAction, +) -> Result<(), SonorustError> { + let Some(guild_id) = guild_id else { + return Ok(()); + }; + + // 自分自身の変更の場合何もしない + if user_id == ctx.cache.current_user().id { + return Ok(()); + }; + + // もしそのサーバーのVCにいない場合何もしない + let manager = songbird::get(ctx).await.unwrap().clone(); + if let None = manager.get(guild_id) { + return Ok(()); + } + + // 自分自身がいるボイスチャンネルの id を取得してそのチャンネルでの変更か確認 + let self_in_vc = { + let guild = match guild_id.to_guild_cached(&ctx.cache) { + Some(guild) => guild, + None => return Ok(()), + }; + + let voice_states: &HashMap = &guild.voice_states; + match voice_states.get(&ctx.cache.current_user().id) { + Some(ch) => ch.channel_id, + None => return Ok(()), + } + }; + + if Some(channel_id) != self_in_vc { + return Ok(()); + } + + // 変更があったユーザー名を取得 できなかった場合リターン + let Ok(user) = user_id.to_user(&ctx.http).await else { + return Ok(()); + }; + + let user_name = user + .nick_in(&ctx.http, guild_id) + .await + .unwrap_or_else(|| user.global_name.unwrap_or_else(|| user.name)); + + // 読み上げているチャンネルを取得 取得できなかった場合リターン + let log_channel = { + let read_channels = READ_CHANNELS.read().unwrap(); + + match read_channels.get(&guild_id) { + Some(ch) => *ch, + None => return Ok(()), + } + }; + + // サーバーデータの取得 + let guild_data = GuildData::from(guild_id).await?; + if guild_data.options.is_entrance_exit_log { + // メッセージを送信 + let msg = match user_action { + UserAction::Entrance => format!("> **{}** さんが参加しました。", user_name), + UserAction::Exit => format!("> **{}** さんが退席しました。", user_name), + }; + + log_channel.say(&ctx.http, msg).await?; + }; + + // チャンネルで読み上げ + if guild_data.options.is_entrance_exit_play { + let lang = SettingsJson::get_bot_lang(); + + let user_name_r = { + use crate::_langrustang_autogen::Lang::*; + + match lang { + Ja => { + let mut text_replace = TextReplace::new(user_name); + text_replace.eng_to_kana(); + + text_replace.as_string() + } + _ => user_name, + } + }; + + let msg = match user_action { + UserAction::Entrance => format_t!("msg.vc_joined", lang, user_name_r), + UserAction::Exit => format_t!("msg.vc_leaved", lang, user_name_r), + }; + + if let Err(why) = + Sbv2Client::play_on_voice_channel(ctx, Some(guild_id), log_channel, user_id, &msg).await + { + log::error!("{}: {}", lang_t!("log.err_send_msg"), why); + }; + } + + Ok(()) +} diff --git a/crates/sonorust_db/Cargo.toml b/crates/sonorust_db/Cargo.toml new file mode 100644 index 0000000..15ca28e --- /dev/null +++ b/crates/sonorust_db/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "sonorust_db" +edition = "2021" +publish = false + +[dependencies] +anyhow.workspace = true +rusqlite.workspace = true +serenity.workspace = true +tokio.workspace = true diff --git a/crates/sonorust_db/src/errors.rs b/crates/sonorust_db/src/errors.rs new file mode 100644 index 0000000..8d63df9 --- /dev/null +++ b/crates/sonorust_db/src/errors.rs @@ -0,0 +1,43 @@ +use std::fmt::Display; + +#[derive(Debug)] +pub enum SonorustDBError { + InitDatabase(rusqlite::Error), + GetGuildData(rusqlite::Error), + UpdateGuildData(rusqlite::Error), + GetUserData(rusqlite::Error), + UpdateUserData(rusqlite::Error), + Unknown(anyhow::Error), +} + +impl Display for SonorustDBError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use SonorustDBError::*; + + match self { + InitDatabase(error) => write!(f, "Failed to init database: {}", error), + GetGuildData(error) => write!(f, "Failed to get guild data: {}", error), + UpdateGuildData(error) => { + write!(f, "Failed to update guild data: {}", error) + } + GetUserData(error) => write!(f, "Failed to get user data: {}", error), + UpdateUserData(error) => { + write!(f, "Failed to update user data: {}", error) + } + Unknown(error) => write!(f, "SonorustDB Unknown Error: {}", error), + } + } +} + +impl std::error::Error for SonorustDBError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + SonorustDBError::InitDatabase(error) => Some(error), + SonorustDBError::GetGuildData(error) => Some(error), + SonorustDBError::UpdateGuildData(error) => Some(error), + SonorustDBError::GetUserData(error) => Some(error), + SonorustDBError::UpdateUserData(error) => Some(error), + SonorustDBError::Unknown(_) => None, + } + } +} diff --git a/crates/sonorust_db/src/guild.rs b/crates/sonorust_db/src/guild.rs new file mode 100644 index 0000000..8256576 --- /dev/null +++ b/crates/sonorust_db/src/guild.rs @@ -0,0 +1,414 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use rusqlite::{params, OptionalExtension}; +use serenity::all::GuildId; +use tokio::sync::{RwLock as TokioRwLock, RwLockWriteGuard as TokioRwLockWriteGuard}; + +use super::DATABASE_CONN; +use crate::errors::SonorustDBError; + +static DB_CACHE: LazyLock>>> = + LazyLock::new(|| TokioRwLock::new(HashMap::new())); + +struct GuildDatabase; + +impl GuildDatabase { + async fn from(guild_id: GuildId) -> Result, SonorustDBError> { + let result = tokio::task::spawn_blocking(move || { + let mut conn = DATABASE_CONN.lock().unwrap(); + let txn = conn.transaction()?; + + // guildtable_id の取り出し + let sql = "SELECT id FROM guild WHERE discord_id = ?1;"; + let result: Option = txn + .query_row(sql, [guild_id.get()], |row| row.get(0)) + .optional()?; + + // データベースになければ返す + let Some(guildtable_id) = result else { + return Ok(None); + }; + + // サーバー辞書 + let mut dict: HashMap = HashMap::new(); + { + let mut stmt = txn.prepare( + "SELECT before_text, after_text FROM guild_dict WHERE guild_table_id = ?1", + )?; + + let mut rows = stmt.query([guildtable_id])?; + + while let Some(row) = rows.next()? { + dict.insert(row.get(0)?, row.get(1)?); + } + } + + // サーバーオプション + let mut options = GuildOptions::default(); + let option_pairs = [ + (&mut options.is_auto_join, "is_auto_join"), + (&mut options.is_dic_onlyadmin, "is_dic_onlyadmin"), + (&mut options.is_entrance_exit_log, "is_entrance_exit_log"), + (&mut options.is_entrance_exit_play, "is_entrance_exit_play"), + (&mut options.is_if_long_fastread, "is_if_long_fastread"), + (&mut options.is_notice_attachment, "is_notice_attachment"), + ]; + + { + let mut stmt = txn.prepare( + " + SELECT id FROM guild_guild_options + WHERE guild_table_id = ?1 + AND guild_option_table_id = (SELECT id FROM guild_option WHERE option_name = ?2); + ", + )?; + + for (option_refm, option_name) in option_pairs { + let result: Option = stmt + .query_row(params![guildtable_id, option_name], |row| row.get(0)) + .optional()?; + + match result { + Some(_) => *option_refm = true, + None => *option_refm = false, + } + } + } + + txn.commit()?; + + Ok(Some(GuildData { + guild_id, + dict: dict, + options: options, + })) + }) + .await; + + match result { + Ok(Ok(value)) => Ok(value), + Ok(Err(err)) => Err(SonorustDBError::UpdateGuildData(err)), + Err(err) => Err(SonorustDBError::Unknown(err.into())), + } + } + + async fn update(guild_data: GuildData) -> Result<(), SonorustDBError> { + let result = tokio::task::spawn_blocking(move || { + let mut conn = DATABASE_CONN.lock().unwrap(); + let txn = conn.transaction()?; + + // guildtable_id の取り出し + let sql = "SELECT id FROM guild WHERE discord_id = ?1;"; + let result: Option = txn + .query_row(sql, [guild_data.guild_id.get()], |row| row.get(0)) + .optional()?; + + let guildtable_id = match result { + Some(id) => id, + None => { + txn.execute( + "INSERT INTO guild (discord_id) VALUES (?1);", + [guild_data.guild_id.get()], + )?; + + txn.query_row(sql, [guild_data.guild_id.get()], |row| row.get(0))? + } + }; + + // サーバー辞書のアップデート + { + txn.execute( + "DELETE FROM guild_dict WHERE guild_table_id = ?1", + [guildtable_id], + )?; + + let mut stmt = txn + .prepare(" + INSERT INTO guild_dict (guild_table_id, before_text, after_text) + VALUES (?1, ?2, ?3) + ON CONFLICT (guild_table_id, before_text) DO NOTHING; + ")?; + + for (before_text, after_text) in guild_data.dict { + stmt.execute(params![guildtable_id, before_text, after_text])?; + } + } + + + // オプションのアップデート + { + let mut insert_stmt = txn.prepare( + " + INSERT INTO guild_guild_options (guild_table_id, guild_option_table_id) + VALUES (?1, + (SELECT (id) FROM guild_option WHERE option_name = ?2)) + ON CONFLICT (guild_table_id, guild_option_table_id) DO NOTHING; + ", + )?; + let mut delete_stmt = txn.prepare( + " + DELETE FROM guild_guild_options + WHERE guild_table_id = ?1 + AND guild_option_table_id = (SELECT (id) FROM guild_option WHERE option_name = ?2); + ", + )?; + + let options = guild_data.options; + let option_pairs = [ + (options.is_auto_join, "is_auto_join"), + (options.is_dic_onlyadmin, "is_dic_onlyadmin"), + (options.is_entrance_exit_log, "is_entrance_exit_log"), + (options.is_entrance_exit_play, "is_entrance_exit_play"), + (options.is_if_long_fastread, "is_if_long_fastread"), + (options.is_notice_attachment, "is_notice_attachment"), + ]; + + for (option_bool, option_name) in option_pairs { + let params = params![guildtable_id, option_name]; + + if option_bool { + insert_stmt.execute(params)?; + } else { + delete_stmt.execute(params)?; + } + } + } + + txn.commit()?; + Ok(()) + }) + .await; + + match result { + Ok(Ok(_)) => Ok(()), + Ok(Err(err)) => Err(SonorustDBError::UpdateGuildData(err)), + Err(err) => Err(SonorustDBError::Unknown(err.into())), + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct GuildOptions { + pub is_dic_onlyadmin: bool, + pub is_auto_join: bool, + pub is_entrance_exit_log: bool, + pub is_entrance_exit_play: bool, + pub is_notice_attachment: bool, + pub is_if_long_fastread: bool, +} + +impl Default for GuildOptions { + fn default() -> Self { + Self { + is_dic_onlyadmin: true, + is_auto_join: false, + is_entrance_exit_log: false, + is_entrance_exit_play: false, + is_notice_attachment: false, + is_if_long_fastread: false, + } + } +} + +#[derive(Debug, Clone)] +pub struct GuildData { + pub guild_id: GuildId, + pub dict: HashMap, + pub options: GuildOptions, +} + +impl GuildData { + pub async fn from(guild_id: GuildId) -> Result { + let cache_data = { + let db_cache = DB_CACHE.read().await; + db_cache.get(&guild_id).map(|i| i.clone()) + }; + + // cacheにあったなら取り出し、なければデータベースから取り出してキャッシュに入れる + let result = match cache_data { + Some(data) => data, + None => { + let data = GuildDatabase::from(guild_id).await?; + + let mut db_cache = DB_CACHE.write().await; + + match &data { + Some(data) => { + db_cache.insert(guild_id, Some(data.clone())); + } + None => { + db_cache.insert(guild_id, None); + } + } + + data + } + }; + + // データベースになければ初期設定を使用 + let guild_data = match result { + Some(data) => data, + None => Self::default_settings(guild_id), + }; + + Ok(guild_data) + } + + pub fn default_settings(guild_id: GuildId) -> GuildData { + Self { + guild_id, + dict: HashMap::new(), + options: GuildOptions::default(), + } + } +} + +#[derive(Debug)] +pub struct GuildDataMut<'a> { + pub guild_id: GuildId, + pub dict: HashMap, + pub options: GuildOptions, + + cache_lock: TokioRwLockWriteGuard<'a, HashMap>>, +} + +impl GuildDataMut<'_> { + pub async fn from<'a>(guild_id: GuildId) -> Result, SonorustDBError> { + let guild_data = GuildData::from(guild_id).await?; + + Ok(GuildDataMut { + guild_id, + dict: guild_data.dict, + options: guild_data.options, + cache_lock: DB_CACHE.write().await, + }) + } + + pub async fn update(self) -> Result<(), SonorustDBError> { + let mut db_cache = self.cache_lock; + + let guild_data = GuildData { + guild_id: self.guild_id, + dict: self.dict, + options: self.options, + }; + + GuildDatabase::update(guild_data.clone()).await?; + db_cache.insert(self.guild_id, Some(guild_data)); + + Ok(()) + } +} + +#[cfg(test)] +mod tests_guild_data_base { + use super::*; + + #[ignore] + #[tokio::test] + async fn test_update() { + let dict = [("A1", "B1"), ("A2", "B2"), ("A3", "B3")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + GuildDatabase::update(GuildData { + guild_id: GuildId::new(123), + dict, + options: GuildOptions { + is_dic_onlyadmin: true, + is_auto_join: false, + is_entrance_exit_log: true, + is_entrance_exit_play: false, + is_notice_attachment: true, + is_if_long_fastread: false, + }, + }) + .await + .unwrap(); + + let dict = [("A4", "B4"), ("A5", "B5"), ("A6", "B6")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + GuildDatabase::update(GuildData { + guild_id: GuildId::new(456), + dict, + options: GuildOptions { + is_dic_onlyadmin: false, + is_auto_join: false, + is_entrance_exit_log: false, + is_entrance_exit_play: true, + is_notice_attachment: true, + is_if_long_fastread: true, + }, + }) + .await + .unwrap() + } + + #[ignore] + #[tokio::test] + async fn test_from() { + for i in [123, 456] { + let guild_data = GuildDatabase::from(GuildId::new(i)).await.unwrap(); + dbg!(guild_data); + } + } +} + +#[cfg(test)] +mod tests_guild_data { + use serenity::all::GuildId; + + use super::*; + + #[ignore] + #[tokio::test] + async fn test_from() { + for i in [1, 123, 456, 1, 123, 456] { + dbg!(GuildData::from(GuildId::new(i)).await.unwrap()); + { + let db_cache = DB_CACHE.read().await; + dbg!(&db_cache); + } + } + } +} + +#[cfg(test)] +mod tests_guilddata_mut { + use serenity::all::GuildId; + + use super::*; + + #[ignore] + #[tokio::test] + async fn test_from() { + for i in [1, 123, 456] { + dbg!(GuildDataMut::from(GuildId::new(i)).await.unwrap()); + } + } + + #[ignore] + #[tokio::test] + async fn test_update() -> anyhow::Result<()> { + let guild_id = GuildId::from(123); + + for i in [true, false, true, false] { + { + let mut guilddata_mut = GuildDataMut::from(guild_id).await?; + dbg!(guilddata_mut.options.is_auto_join); + + guilddata_mut.options.is_auto_join = i; + guilddata_mut.update().await?; + } + + let guild_data = GuildData::from(guild_id).await?; + dbg!(guild_data.options.is_auto_join); + } + + Ok(()) + } +} diff --git a/crates/sonorust_db/src/lib.rs b/crates/sonorust_db/src/lib.rs new file mode 100644 index 0000000..cf66b3c --- /dev/null +++ b/crates/sonorust_db/src/lib.rs @@ -0,0 +1,122 @@ +mod errors; +mod guild; +mod user; + +pub use errors::SonorustDBError; +pub use guild::{GuildData, GuildDataMut, GuildOptions}; +pub use user::{UserData, UserDataMut}; + +use std::{ + path::PathBuf, + sync::{LazyLock, Mutex}, +}; + +use rusqlite::{params, Connection}; + +static DATABASE_PATH: LazyLock = LazyLock::new(|| PathBuf::from("./appdata/database.db")); + +static DATABASE_CONN: LazyLock> = LazyLock::new(|| { + std::fs::create_dir_all(DATABASE_PATH.ancestors().nth(1).unwrap()).expect("Can't open file."); + + let mut conn = Connection::open(DATABASE_PATH.as_path()).expect("Can't open file"); + init_database(&mut conn).expect("Failed to init database."); + Mutex::new(conn) +}); + +pub fn init_database(conn: &mut Connection) -> Result<(), SonorustDBError> { + let mut result = || { + let txn = conn.transaction()?; + + // 設定の変更 + txn.execute("PRAGMA foreign_keys = ON;", ())?; + + let sqls = [ + // user table + " + CREATE TABLE IF NOT EXISTS user ( + id INTEGER PRIMARY KEY, + discord_id INTEGER NOT NULL UNIQUE, + model_name TEXT NOT NULL, + speaker_name TEXT NOT NULL, + style_name TEXT NOT NULL, + length REAL NOT NULL + ); + ", + // guild table + " + CREATE TABLE IF NOT EXISTS guild ( + id INTEGER PRIMARY KEY, + discord_id INTEGER NOT NULL UNIQUE + ); + ", + // guild_option table + " + CREATE TABLE IF NOT EXISTS guild_option ( + id INTEGER PRIMARY KEY, + option_name TEXT NOT NULL UNIQUE + ); + ", + // guild と guild_option の中間テーブル + " + CREATE TABLE IF NOT EXISTS guild_guild_options ( + id INTEGER PRIMARY KEY, + guild_table_id INTEGER NOT NULL, + guild_option_table_id INTEGER NOT NULL, + + FOREIGN KEY (guild_table_id) REFERENCES guild(id), + FOREIGN KEY (guild_option_table_id) REFERENCES guild_option(id), + UNIQUE (guild_table_id, guild_option_table_id) + ); + ", + // guild_dict table + " + CREATE TABLE IF NOT EXISTS guild_dict ( + id INTEGER PRIMARY KEY, + guild_table_id INTEGER NOT NULL, + before_text TEXT NOT NULL, + after_text TEXT NOT NULL, + + FOREIGN KEY (guild_table_id) REFERENCES guild(id), + UNIQUE (guild_table_id, before_text) + ); + ", + ]; + + for i in sqls { + txn.execute(i, ())?; + } + + // ギルドオプションの追加 + let guild_options = [ + "is_dic_onlyadmin", + "is_auto_join", + "is_entrance_exit_log", + "is_entrance_exit_play", + "is_notice_attachment", + "is_if_long_fastread", + ]; + + for i in guild_options { + txn.execute( + "INSERT INTO guild_option (option_name) VALUES (?1) ON CONFLICT DO NOTHING", + params![i], + )?; + } + + txn.commit()?; + Ok(()) + }; + + result().map_err(|err| SonorustDBError::InitDatabase(err)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[test] + fn test_init_database() { + let _ = *DATABASE_CONN; + } +} diff --git a/crates/sonorust_db/src/user.rs b/crates/sonorust_db/src/user.rs new file mode 100644 index 0000000..5a90274 --- /dev/null +++ b/crates/sonorust_db/src/user.rs @@ -0,0 +1,266 @@ +use std::{collections::HashMap, sync::LazyLock}; + +use rusqlite::{params, OptionalExtension}; +use serenity::all::UserId; +use tokio::sync::{RwLock as TokioRwLock, RwLockWriteGuard as TokioRwLockWriteGuard}; + +use super::DATABASE_CONN; +use crate::errors::SonorustDBError; + +static DB_CACHE: LazyLock>>> = + LazyLock::new(|| TokioRwLock::new(HashMap::new())); + +struct UserDatabase; + +impl UserDatabase { + async fn from(user_id: UserId) -> Result, SonorustDBError> { + let result = tokio::task::spawn_blocking(move || { + let mut conn = DATABASE_CONN.lock().unwrap(); + let txn = conn.transaction()?; + + let result = txn + .query_row( + "SELECT model_name, speaker_name, style_name, length + FROM user WHERE discord_id = ?1;", + [user_id.get()], + |row| { + Ok(UserData { + user_id, + model_name: row.get(0)?, + speaker_name: row.get(1)?, + style_name: row.get(2)?, + length: row.get(3)?, + }) + }, + ) + .optional()?; + + Ok(result) + }) + .await; + + match result { + Ok(Ok(value)) => Ok(value), + Ok(Err(err)) => Err(SonorustDBError::GetUserData(err)), + Err(err) => Err(SonorustDBError::Unknown(err.into())), + } + } + + async fn update(user_data: UserData) -> Result<(), SonorustDBError> { + let result = tokio::task::spawn_blocking(move || { + let mut conn = DATABASE_CONN.lock().unwrap(); + let txn = conn.transaction()?; + + txn.execute( + "INSERT OR REPLACE INTO + user (discord_id, model_name, speaker_name, style_name, length) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![ + user_data.user_id.get(), + user_data.model_name, + user_data.speaker_name, + user_data.style_name, + user_data.length + ], + )?; + + txn.commit()?; + Ok(()) + }) + .await; + + match result { + Ok(Ok(_)) => Ok(()), + Ok(Err(err)) => Err(SonorustDBError::UpdateUserData(err)), + Err(err) => Err(SonorustDBError::Unknown(err.into())), + } + } +} + +#[derive(Debug, Clone)] +pub struct UserData { + pub user_id: UserId, + pub model_name: String, + pub speaker_name: String, + pub style_name: String, + pub length: f64, +} + +impl UserData { + pub async fn from(user_id: UserId) -> Result { + let cache_data = { + let db_cache = DB_CACHE.read().await; + db_cache.get(&user_id).map(|i| i.clone()) + }; + + // cacheにあったなら取り出し、なければデータベースから取り出してキャッシュに入れる + let result = match cache_data { + Some(data) => data, + None => { + let data = UserDatabase::from(user_id).await?; + + let mut db_cache = DB_CACHE.write().await; + + match &data { + Some(data) => { + db_cache.insert(user_id, Some(data.clone())); + } + None => { + db_cache.insert(user_id, None); + } + } + + data + } + }; + + // データベースになければ初期設定を使用 + let user_data = match result { + Some(data) => data, + None => Self::default_settings(user_id), + }; + + Ok(user_data) + } + + pub fn default_settings(user_id: UserId) -> UserData { + Self { + user_id, + model_name: "None".to_string(), + speaker_name: "None".to_string(), + style_name: "None".to_string(), + length: 1.0, + } + } +} + +#[derive(Debug)] +pub struct UserDataMut<'a> { + pub user_id: UserId, + pub model_name: String, + pub speaker_name: String, + pub style_name: String, + pub length: f64, + + cache_lock: TokioRwLockWriteGuard<'a, HashMap>>, +} + +impl UserDataMut<'_> { + pub async fn from<'a>(user_id: UserId) -> Result, SonorustDBError> { + let user_data = UserData::from(user_id).await?; + + Ok(UserDataMut { + user_id, + model_name: user_data.model_name, + speaker_name: user_data.speaker_name, + style_name: user_data.style_name, + length: user_data.length, + cache_lock: DB_CACHE.write().await, + }) + } + + pub async fn update(self) -> Result<(), SonorustDBError> { + let mut db_cache = self.cache_lock; + + let user_data = UserData { + user_id: self.user_id, + model_name: self.model_name, + speaker_name: self.speaker_name, + style_name: self.style_name, + length: self.length, + }; + + UserDatabase::update(user_data.clone()).await?; + db_cache.insert(self.user_id, Some(user_data)); + + Ok(()) + } +} + +#[cfg(test)] +mod tests_user_data_base { + use super::*; + + #[ignore] + #[tokio::test] + async fn test_update() { + UserDatabase::update(UserData { + user_id: 1.into(), + model_name: "model_name1".to_string(), + speaker_name: "speaker_name2".to_string(), + style_name: "style_name3".to_string(), + length: 1.5, + }) + .await + .unwrap(); + } + + #[ignore] + #[tokio::test] + async fn test_from() { + let user_data = UserDatabase::from(1.into()).await.unwrap(); + dbg!(user_data); + + let user_data = UserDatabase::from(2.into()).await.unwrap(); + dbg!(user_data); + } +} + +#[cfg(test)] +mod tests_user_data { + use serenity::all::UserId; + + use super::*; + + #[ignore] + #[tokio::test] + async fn test_from() { + for i in [1, 123, 456, 1, 123, 456] { + dbg!(UserData::from(UserId::new(i)).await.unwrap()); + { + let db_cache = DB_CACHE.read().await; + dbg!(&db_cache); + } + } + } +} + +#[cfg(test)] +mod tests_userdata_mut { + use serenity::all::UserId; + + use super::*; + + #[ignore] + #[tokio::test] + async fn test_from() { + for i in [1, 123, 456, 1, 123, 456] { + dbg!(UserDataMut::from(UserId::new(i)).await.unwrap()); + { + let db_cache = DB_CACHE.read().await; + dbg!(&db_cache); + } + } + } + + #[ignore] + #[tokio::test] + async fn test_update() -> anyhow::Result<()> { + let user_id = UserId::new(123); + + for i in ["Name1", "Name2", "Name3", "Name4"] { + { + let mut userdata_mut = UserDataMut::from(user_id).await?; + dbg!(&userdata_mut.model_name); + + userdata_mut.model_name = i.to_string(); + userdata_mut.update().await?; + } + + let user_data = UserData::from(user_id).await?; + dbg!(&user_data.model_name); + } + + Ok(()) + } +} diff --git a/crates/sonorust_logger/Cargo.toml b/crates/sonorust_logger/Cargo.toml new file mode 100644 index 0000000..20446ff --- /dev/null +++ b/crates/sonorust_logger/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "sonorust_logger" +edition = "2021" +publish = false + +[dependencies] +log.workspace = true +env_logger.workspace = true +chrono.workspace = true diff --git a/crates/sonorust_logger/src/lib.rs b/crates/sonorust_logger/src/lib.rs new file mode 100644 index 0000000..77c7d59 --- /dev/null +++ b/crates/sonorust_logger/src/lib.rs @@ -0,0 +1,84 @@ +use std::io::Write; + +use env_logger::{Builder, Env}; +use log::{Level, LevelFilter}; + +pub fn setup_logger() { + let env_level = Env::default().default_filter_or("info"); + let env_level_str = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string()); + let env_levelfilter = env_level_str.parse().unwrap_or_else(|_| LevelFilter::Info); + + Builder::from_env(env_level) + .filter_level(LevelFilter::Off) + .filter_module("sonorust", env_levelfilter) + .filter_module("sonorust_db", env_levelfilter) + .filter_module("sonorust_logger", env_levelfilter) + .filter_module("engtokana", env_levelfilter) + .filter_module("setting_inputter", env_levelfilter) + .format(move |buf, record| { + let level = record.level(); + let level_color = match level { + Level::Error => "\x1B[31m", // 赤 + Level::Warn => "\x1B[33m", // 黄 + Level::Info => "\x1B[32m", // 緑 + Level::Debug => "\x1B[34m", // 青 + Level::Trace => "\x1B[35m", // マゼンタ + }; + + let space_len = 5 - record.level().as_str().len(); + let level_name = record.level().to_string() + &" ".repeat(space_len); + + let reset = "\x1B[0m"; + let green = "\x1B[32m"; + + // 表示レベルが Debug かどうかで動作を変える + if env_levelfilter == LevelFilter::Debug { + buf.write_fmt(format_args!( + "{level_color}{}{reset} | {green}{}{reset} | {} - {}:{}\n", + level_name, + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.args(), + record.file().unwrap_or("unknown"), + record.line().unwrap_or(0), + )) + } else { + buf.write_fmt(format_args!( + "{level_color}{}{reset} | {green}{}{reset} | {}\n", + level_name, + chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), + record.args() + )) + } + }) + .init(); +} + +#[cfg(test)] +mod tests { + + #[ignore] + #[test] + fn log_test() { + std::env::set_var("RUST_LOG", "trace"); + super::setup_logger(); + + log::trace!("trace_log"); + log::debug!("debug_log"); + log::info!("info_log"); + log::warn!("warn_log"); + log::error!("error_log"); + } + + #[ignore] + #[test] + fn log_test_debug() { + std::env::set_var("RUST_LOG", "debug"); + super::setup_logger(); + + log::trace!("trace_log"); + log::debug!("debug_log"); + log::info!("info_log"); + log::warn!("warn_log"); + log::error!("error_log"); + } +} diff --git a/lang/bot_lang.yaml b/lang/bot_lang.yaml new file mode 100644 index 0000000..b9a58f7 --- /dev/null +++ b/lang/bot_lang.yaml @@ -0,0 +1,535 @@ +#____ Bot Commands ____# + +# Ping +ping.command.name: + all: ping + +ping.command.description: + all: Pong! + +ping.embed.title: + all: Pong! + +ping.embed.measuring: + ja: 計測中... + en: Measuring... + +ping.embed.measured: + all: "Time: {:?}" + +# Help +help.command.name: + all: help + +help.command.description: + ja: コマンドのヘルプを表示します。 + en: Shows help for the command. + +help.embed.title: + ja: このBOTのヘルプ + en: Help for this BOT + +# Now +now.command.name: + all: now + +now.command.description: + ja: 現在使用しているモデル情報を表示します。 + en: Displays the model information currently being used. + +now.embed.title: + ja: "**{}** の現在のモデル情報" + en: "Current model information for **{}**" + +now.embed.model: + ja: モデル + en: Model + +now.embed.speaker: + ja: 話者 + en: Speaker + +now.embed.style: + ja: スタイル + en: Style + +now.embed.speech_rate: + ja: 読み上げ速度 + en: Speech Rate + +# Join +join.command.name: + all: join + +join.command.description: + ja: 使用した人が接続しているボイスチャンネルに接続します。 + en: It will connect you to the same voice channel as the person who used it. + +join.connected: + ja: ボイスチャンネルに接続しました。 + en: Connected to the voice channel. + +join.already: + ja: すでにボイスチャンネルに参加しています。 + en: Already in a voice channel. + +join.after_connecting: + ja: ボイスチャンネルに接続してから使用してください。 + en: Connect to a voice channel before using. + +join.cannot_connect: + ja: ボイスチャンネルに接続できませんでした。 + en: Could not connect to voice channel. + +# Leave +leave.command.name: + all: leave + +leave.command.description: + ja: 接続しているボイスチャンネルから退出します。 + en: Leaves the voice channel you are connected to. + +leave.disconnected: + ja: ボイスチャンネルから退出しました。 + en: Left the voice channel. + +leave.already: + ja: ボイスチャンネルに接続していません。 + en: Not connected to a voice channel. + +leave.cannot_disconnect: + ja: ボイスチャンネルから退出できませんでした。 + en: Could not to leave voice channel. + +# Model +model.command.name: + all: model + +model.command.description: + ja: 使用できるモデルの変更メニューを表示します。 + en: Displays the change menu for the available models. + +model.embed.title: + ja: 使用できるモデル一覧 + en: Available Models + +model.changed: + ja: "使用するモデルを **{}** に変更しました。" + en: "The model used has been changed to **{}**." + +# Speaker +speaker.command.name: + all: speaker + +speaker.command.description: + ja: 現在のモデルの話者変更メニューを表示します。 + en: Displays the speaker change menu for the current model. + +speaker.embed.title: + ja: モデル '{}' の使用できる話者一覧 + en: List of available speakers for model '{}' + +speaker.changed_with_model: + ja: |- + 使用するモデルを **{}** から **{}** に変更しました。 + 使用する話者を **{}** に変更しました。 + + en: |- + The model used has been changed from **{}** to **{}**. + The speaker used has been changed to **{}**. + +speaker.changed: + ja: "使用する話者を **{}** に変更しました。" + en: "The speaker used has been changed to **{}**." + +# Style +style.command.name: + all: style + +style.command.description: + ja: 現在のモデルのスタイル変更メニューを表示します。 + en: Displays the style change menu for the current model. + +style.embed.title: + ja: モデル '{}' の使用できるスタイル一覧 + en: List of available styles for model '{}' + +style.changed_with_model: + ja: |- + 使用するモデルを **{}** から **{}** に変更しました。 + 使用するスタイルを **{}** に変更しました。 + + en: |- + The model used has been changed from **{}** to **{}**. + The style used has been changed to **{}**. + +style.changed: + ja: "使用する話者を **{}** に変更しました。" + en: "The style used has been changed to **{}**." + +# Server +server.command.name: + all: server + +server.command.description: + ja: サーバーに関する設定画面を表示します。 + en: Displays the server settings screen. + +server.embed.title: + ja: "{} の現在設定" + en: "Current setting of {}" + +server.components.placeholder: + ja: 変更したい設定を選択してください... + en: Choose the setting you want to change... + +server.changed: + ja: '"{}" を **{}** に変更しました。' + en: '"{}" changed to **{}**.' + +# Dict +dict.command.name: + all: dict + +dict.command.description: + ja: サーバー辞書メニューを表示します。 + en: Displays the server dictionary menu. + +dict.embed.title: + ja: "{} のサーバー辞書設定" + en: "Server dictionary settings for {}" + +dict.unregistered: + ja: このサーバーではまだ何も登録されていません。 + en: There is nothing registered on this server yet. + +dict.too_many: + ja: 登録されている単語が多すぎたため表示できませんでした。 + en: There are too many words registered to display. + +dict.modal.add.title: + ja: 単語と読み方の登録 + en: Register words and readings + +dict.modal.add.word: + ja: 単語 + en: word + +dict.modal.add.readings: + ja: 読み方 + en: readings + +dict.modal.add.set: + ja: |- + 読み方を設定しました + 単語: **{}** 読み方: **{}** + + en: |- + Reading set + Word: **{}** Reading: **{}** + +dict.modal.remove.title: + ja: 登録した単語の削除 + en: Deleting a registered word + +dict.modal.remove.field: + ja: 削除したい単語 + en: Word want to delete + +dict.modal.remove.deleted: + ja: |- + 辞書から読み方を削除しました + 削除した単語: **{}** + + en: |- + Readings have been removed from the dictionary. + Deleted word: **{}** + +dict.modal.remove.not_found: + ja: "辞書に 単語: **{}** は存在しませんでした。" + en: "The word: **{}** was not found in the dictionary." + +dict.label.add: + all: add + +dict.label.remove: + all: remove + +# Reload +reload.command.name: + all: reload + +reload.command.description: + ja: |- + SBV2のモデルを再読み込みします。 + BOTの所有者のみ使用できます。 + + en: |- + Reloads the SBV2 model. + Only available to bot owners. + +reload.executed: + ja: モデルを再読み込みしました。 + en: The model has been reloaded. + +# Length +length.command.name: + all: length + +length.command.description: + ja: 読み上げ速度を変更します。 + en: Change the speech rate. + +length.option.length: + all: length + +length.option.length.description: + ja: 読み上げる速度 + en: Speech Rate + +length.changed: + ja: 読み上げ速度を **{}** に変更しました。 + en: The speech rate has been changed to **{}**. + +length.usage: + ja: "使用方法: `{}length (読み上げ速度)`" + en: "Usage: `{}length (Speech Rate)`" + +length.not_num: + ja: 読み上げ速度は数字を指定してください。 + en: Specify the speech rate by entering a number. + +# Wav +wav.command.name: + all: wav + +wav.command.description: + ja: 指定した内容を生成してその音声ファイルを添付したメッセージを送信します。 + en: It will generate the content you specify and send a message with the audio file attached. + +wav.option.content: + all: content + +wav.option.content.description: + ja: 生成したい文章を入力します。 + en: Enter the text you want to generate. + +wav.usage: + ja: "使用方法: `{}wav (生成したいテキスト)`" + en: "Usage: `{}wav (text to generate)`" + +wav.fail_infer: + ja: APIが起動していないため音声を生成できませんでした。 + en: Audio could not be generated because the API was not launch. + +#____ Bot Messages ____# +msg.attachments: + ja: 添付ファイル + en: Attachments + +msg.vc_joined: + ja: "{} さんが参加しました。" + en: "{} has joined." + +msg.vc_leaved: + ja: "{} さんが退席しました。" + en: "{} has left." + +msg.omitted: + ja: "{}、以下略。" + en: "{}, omitted." + +msg.only_use_guild_1: + ja: サーバー限定の機能です。 + en: This is a server-only feature. + +msg.only_use_guild_2: + ja: このコマンドはサーバー以外では使用できません。 + en: This command can only be used on the server. + +msg.only_admin: + ja: この機能はサーバーの管理者のみ利用可能です。 + en: This feature is only available to server admin. + +msg.only_owner: + ja: このコマンドはBOTの所有者のみ利用可能です。 + en: This command is only available to the bot owner. + +msg.failed.get: + ja: データの取得に失敗しました。 + en: Failed to acquire data. + +msg.failed.update: + ja: データの更新に失敗しました。 + en: Failed to update data. + +msg.error: + ja: エラーが発生しました。 + en: An error has occurred. + +msg.failed.infer: + ja: |- + APIが起動していないため音声を生成できません。 + ボイスチャンネルから退出しました。 + + en: |- + Audio cannot be generated because the API is not running. + Left the voice channel. + +#____ Guild Settings value ____# + +# Value +guild.is_auto_join: + all: is_auto_join + +guild.is_dic_onlyadmin: + all: is_dic_onlyadmin + +guild.is_entrance_exit_log: + all: is_entrance_exit_log + +guild.is_entrance_exit_play: + all: is_entrance_exit_play + +guild.is_notice_attachment: + all: is_notice_attachment + +guild.is_if_long_fastread: + all: is_if_long_fastread + +# Description +guild.desc.is_auto_join: + ja: VCへの自動参加 + en: Automatically join VC + +guild.desc.is_dic_onlyadmin: + ja: 辞書の編集権限を管理者に限定する + en: Limit dictionary editing privileges to admin + +guild.desc.is_entrance_exit_log: + ja: 入退出時のテキストログ + en: Text log of entry and exit + +guild.desc.is_entrance_exit_play: + ja: 入退出時の音声通知 + en: Voice notification when entering and exiting + +guild.desc.is_notice_attachment: + ja: 添付ファイルの音声通知 + en: Voice notification for attachments + +guild.desc.is_if_long_fastread: + ja: 長い文章の場合早めに読み上げる + en: Read long sentences quickly + +#____ Log Messages ____# + +log.cant_open_file: + all: Can't open File. + +log.cant_create_file: + all: Can't create File. + +log.err_create_db: + all: Error creating Database + +log.not_implemented_command: + all: Not Implemented Command + +log.not_implemented_customid: + all: Not Implemented Custom ID + +log.not_implemented_value: + all: Not Implemented Value + +log.err_send_msg: + all: Error sending message + +log.cannot_res_interaction: + all: Cannot respond to interaction + +log.fail_regist_commands: + all: Failed to registering commands + +log.fail_inter_not_launch: + all: Failed to infer because API not launch. + +log.fail_conn_api: + all: Failed to connect API. + +log.fail_join_vc: + all: Failed to join voice channel + +log.fail_leave_vc: + all: Failed to leave Voice channel + +log.fail_get_data: + all: Failed to get data. + +log.fail_get_guild: + all: Failed to get guild. + +log.fail_update_guilddata: + all: Failed to get Guilddata. + +log.fail_get_user: + all: Failed to get User. + +log.fail_get_userdata: + all: Failed to get Userdata. + +log.fail_update_userdata: + all: Failed to update Userdata. + +log.fail_adj_vol: + all: Failed to adjusting volume + +log.fail_ch_queue: + all: Failed to get channel queue. + +#____ Component CustomId ____# + +customid.change_server_settings: + all: change_server_settings + +customid.dict.add: + all: dict_add + +customid.dict.remove: + all: dict_remove + +customid.select.model: + all: select_model + +customid.page.model.forward: + all: model_pageforward + +customid.page.model.number: + all: model_page_number + +customid.page.model.back: + all: model_pageback + +customid.select.speaker: + all: select_speaker + +customid.page.speaker.forward: + all: speaker_pageforward + +customid.page.speaker.number: + all: speaker_page_number + +customid.page.speaker.back: + all: speaker_pageback + +customid.select.style: + all: select_style + +customid.page.style.forward: + all: style_pageforward + +customid.page.style.number: + all: style_page_number + +customid.page.style.back: + all: style_pageback