From 278d7cf05d03af9ff9191d6f1179688023331499 Mon Sep 17 00:00:00 2001 From: spushk Date: Fri, 15 May 2026 22:27:28 +0500 Subject: [PATCH] just throwin there --- .cargo/config.toml | 12 + .cargo/container_release.toml | 3 + .containerignore | 8 + .gitignore | 6 + Cargo.lock | 3066 +++++++++++++++++ Cargo.toml | 69 + Containerfile | 65 + Makefile.toml | 52 + README.md | 232 ++ adsbot.conf.example/leadbot/adsbot.toml | 10 + .../leadbot/features/echo.d/echo.toml | 2 + .../leadbot/features/formlead.d/example.toml | 17 + .../leadbot/features/vkadslead.d/example.toml | 23 + diesel.toml | 13 + migrations/.keep | 0 .../down.sql | 3 + .../up.sql | 19 + .../down.sql | 2 + .../up.sql | 5 + rust-toolchain.toml | 3 + rustfmt.toml | 5 + src/apis/mod.rs | 1 + src/apis/vkads/api.rs | 273 ++ src/apis/vkads/config.rs | 16 + src/apis/vkads/mappings/contact_info.rs | 24 + src/apis/vkads/mappings/lead_answer.rs | 11 + src/apis/vkads/mappings/lead_answer_option.rs | 7 + src/apis/vkads/mappings/leads_list_element.rs | 16 + src/apis/vkads/mappings/mod.rs | 27 + src/apis/vkads/mod.rs | 19 + src/apis/vkads/request.rs | 104 + src/config.rs | 22 + src/db/mod.rs | 44 + src/features/echo/mod.rs | 74 + src/features/formlead/mod.rs | 1 + src/features/helpers/mod.rs | 1 + src/features/mod.rs | 406 +++ src/features/vkadslead/config.rs | 79 + src/features/vkadslead/db/mod.rs | 85 + src/features/vkadslead/db/schema.rs | 37 + src/features/vkadslead/mod.rs | 552 +++ src/helpers/formatters.rs | 37 + src/helpers/mod.rs | 3 + src/helpers/reqwest_pool.rs | 24 + src/helpers/serializers.rs | 18 + src/lib.rs | 6 + src/main.rs | 69 + src/telegram/config.rs | 24 + src/telegram/db/mod.rs | 1 + src/telegram/db/schema.rs | 37 + src/telegram/mod.rs | 5 + src/telegram/tree_root.rs | 74 + 52 files changed, 5712 insertions(+) create mode 100644 .cargo/config.toml create mode 100644 .cargo/container_release.toml create mode 100644 .containerignore create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Containerfile create mode 100644 Makefile.toml create mode 100644 README.md create mode 100644 adsbot.conf.example/leadbot/adsbot.toml create mode 100644 adsbot.conf.example/leadbot/features/echo.d/echo.toml create mode 100644 adsbot.conf.example/leadbot/features/formlead.d/example.toml create mode 100644 adsbot.conf.example/leadbot/features/vkadslead.d/example.toml create mode 100644 diesel.toml create mode 100644 migrations/.keep create mode 100644 migrations/2025-09-10-144245_init_feature_vkadslead/down.sql create mode 100644 migrations/2025-09-10-144245_init_feature_vkadslead/up.sql create mode 100644 migrations/2025-09-10-144305_init_integration_telegram/down.sql create mode 100644 migrations/2025-09-10-144305_init_integration_telegram/up.sql create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 src/apis/mod.rs create mode 100644 src/apis/vkads/api.rs create mode 100644 src/apis/vkads/config.rs create mode 100644 src/apis/vkads/mappings/contact_info.rs create mode 100644 src/apis/vkads/mappings/lead_answer.rs create mode 100644 src/apis/vkads/mappings/lead_answer_option.rs create mode 100644 src/apis/vkads/mappings/leads_list_element.rs create mode 100644 src/apis/vkads/mappings/mod.rs create mode 100644 src/apis/vkads/mod.rs create mode 100644 src/apis/vkads/request.rs create mode 100644 src/config.rs create mode 100644 src/db/mod.rs create mode 100644 src/features/echo/mod.rs create mode 100644 src/features/formlead/mod.rs create mode 100644 src/features/helpers/mod.rs create mode 100644 src/features/mod.rs create mode 100644 src/features/vkadslead/config.rs create mode 100644 src/features/vkadslead/db/mod.rs create mode 100644 src/features/vkadslead/db/schema.rs create mode 100644 src/features/vkadslead/mod.rs create mode 100644 src/helpers/formatters.rs create mode 100644 src/helpers/mod.rs create mode 100644 src/helpers/reqwest_pool.rs create mode 100644 src/helpers/serializers.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/telegram/config.rs create mode 100644 src/telegram/db/mod.rs create mode 100644 src/telegram/db/schema.rs create mode 100644 src/telegram/mod.rs create mode 100644 src/telegram/tree_root.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..f5de1ea --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[build] +rustflags = ["--cfg=has_std", "-Zthreads=6"] + +[target.x86_64-unknown-linux-gnu] +rustflags = ["-Csplit-debuginfo=unpacked", "-Clink-arg=-fuse-ld=mold"] + +[profile.release.build-override] +inherits = "release" + +[registries.Technologygeek] +index = "sparse+https://source.technologygeek.ru/api/packages/Technologygeek/cargo/" +credential-provider = ["cargo:token"] diff --git a/.cargo/container_release.toml b/.cargo/container_release.toml new file mode 100644 index 0000000..a2dcc90 --- /dev/null +++ b/.cargo/container_release.toml @@ -0,0 +1,3 @@ +[build] +rustflags = ["--cfg=has_std", "-Zthreads=6", "-Zlocation-detail=none", "-Cpanic=abort"] +target = ["x86_64-unknown-linux-musl"] diff --git a/.containerignore b/.containerignore new file mode 100644 index 0000000..662ed0a --- /dev/null +++ b/.containerignore @@ -0,0 +1,8 @@ +Containerfile +.envrc +target +Cargo.lock +dev-volumes +adsbot.conf.d.example +adsbot.log +adsbot.db diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..868141d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +/dev-volumes +.envrc +adsbot.db +adsbot.log +adsbot.conf diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..687bd21 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3066 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adsbot" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "const-str", + "deadpool", + "diesel", + "diesel_migrations", + "libsqlite3-sys", + "oauth2", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_urlencoded", + "teloxide", + "thiserror 2.0.18", + "tokio", + "toml 1.1.2+spec-1.1.0", + "tracing", + "tracing-subscriber", + "urlencoding", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[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 = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "aquamarine" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f50776554130342de4836ba542aa85a4ddb361690d7e8df13774d7284c3d5c2" +dependencies = [ + "include_dir", + "itertools", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "ar_archive_writer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b" +dependencies = [ + "object", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[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 = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "colored" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "const-str" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "deadpool" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "883466cb8db62725aee5f4a6011e8a5d42912b42632df32aad57fc91127c6e04" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "serde", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2657f61fb1dd8bf37a8d51093cc7cee4e77125b22f7753f49b289f831bec2bae" +dependencies = [ + "tokio", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "diesel" +version = "2.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9940fb8467a0a06312218ed384185cb8536aa10d8ec017d0ce7fad2c1bd882d5" +dependencies = [ + "chrono", + "diesel_derives", + "downcast-rs", + "libsqlite3-sys", + "sqlite-wasm-rs", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" +dependencies = [ + "diesel_table_macro_syntax", + "dsl_auto_type", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "diesel_migrations" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" +dependencies = [ + "syn", +] + +[[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 = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dptree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db96968fcf52fe063a98c75df1d1f2b1fba304e7ae29b72fdc81c1165b7e2fd0" +dependencies = [ + "colored", + "futures", +] + +[[package]] +name = "dsl_auto_type" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" +dependencies = [ + "darling 0.21.3", + "either", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erasable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437cfb75878119ed8265685c41a115724eae43fb7cc5a0bf0e4ecc3b803af1c4" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libsqlite3-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f111c8c41e7c61a49cd34e44c7619462967221a6443b0ec299e0ac30cfb9b1" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "migrations_internals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "migrations_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[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 = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "oauth2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" +dependencies = [ + "base64", + "chrono", + "getrandom 0.2.17", + "http", + "rand 0.8.6", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror 1.0.69", + "url", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rc-box" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897fecc9fac6febd4408f9e935e86df739b0023b625e610e0357535b9c8adad0" +dependencies = [ + "erasable", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsqlite-vfs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a1f2315036ef6b1fbacd1972e8ee7688030b0a2121edfc2a6550febd41574d" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.18", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[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_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b2c760607300407ddeaee518acf28c795661b7108c75421303dbefb237d3a36" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.61.2", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "takecell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + +[[package]] +name = "teloxide" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84992abeed3ae42e8401b25d266d12bcba1def0abe59d22f6b9781167545f71e" +dependencies = [ + "aquamarine", + "bytes", + "derive_more", + "dptree", + "either", + "futures", + "log", + "mime", + "pin-project", + "serde", + "serde_json", + "teloxide-core", + "teloxide-macros", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "teloxide-core" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7a34ca8e971fa892e633858c07547fe138ef4a02e4a4eaa1d35e517d6e0bc4" +dependencies = [ + "bitflags", + "bytes", + "chrono", + "derive_more", + "either", + "futures", + "log", + "mime", + "once_cell", + "pin-project", + "rc-box", + "reqwest", + "rgb", + "serde", + "serde_json", + "serde_with", + "stacker", + "take_mut", + "takecell", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "teloxide-macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300fadcaf0c182f19b5ca10bf23a45dc9a48925f00c704405fd90ee2c03942f9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +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.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "chrono", + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[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 = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[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.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..aeb8433 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "adsbot" +version = "0.1.0" +rust-version = "1.95" +edition = "2024" +license = "MIT" +license-file = "LICENSE" +authors = ["Svyatoslav Pushkarev "] +publish = false + +[dependencies] +## Rt +tokio.version = "^1" +tokio.features = ["full"] +deadpool.version = "^0.13" +deadpool.features = ["rt_tokio_1", "serde"] +async-trait.version = "^0.1" +## Logging +tracing.version = "^0.1" +tracing.features = ["max_level_debug", "release_max_level_info"] +tracing-subscriber.version = "^0.3" +tracing-subscriber.features = ["chrono"] +## Database +diesel.version = "^2" +diesel.features = ["chrono", "sqlite"] +diesel_migrations.version = "^2" +diesel_migrations.features = ["sqlite"] +libsqlite3-sys.version = "^0" +libsqlite3-sys.features = ["bundled"] +## Marshal +serde.version = "^1" +serde.features = ["derive"] +serde_json.version = "^1" +toml.version = "^1" +serde_urlencoded.version = "^0.7" +urlencoding.version = "^2" +## Net +teloxide.version = "^0.17" +teloxide.default-features = false +teloxide.features = ["tracing", "nightly", "ctrlc_handler", "rustls", "macros"] +oauth2.version = "^5" +reqwest.version = "^0.12" +reqwest.default-features = false +reqwest.features = ["charset", "http2", "rustls-tls", "json"] +## Util +chrono.version = "^0.4" +regex.version = "^1" +## Codegen +thiserror.version = "^2" +const-str.version = "^1" + + +[profile.dev] +opt-level = 0 +lto = "off" +codegen-units = 256 +panic = "unwind" +debug = "full" + +[profile.dev.package."*"] +opt-level = 3 + +[profile.release] +opt-level = 3 +lto = "fat" +codegen-units = 1 +panic = "abort" +debug = "none" +strip = "symbols" diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..e36d24c --- /dev/null +++ b/Containerfile @@ -0,0 +1,65 @@ +FROM source.spushk.ru/rustyworks/rust-alpine-mimalloc:1.95 as build +WORKDIR /build +ENV OPENSSL_DIR=/usr +ENV TARGET_CC=x86_64-linux-musl-clang +ENV CARGO_BUILD_TARGET=x86_64-unknown-linux-musl +COPY .cargo .cargo +COPY rust-toolchain.toml rust-toolchain.toml +RUN --mount=type=secret,id=cargo-token \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cat /run/secrets/cargo-token | cargo login --quiet --registry Technologygeek + +FROM build as planner +WORKDIR /build +COPY . . +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo chef prepare --recipe-path=recipe.json + +FROM build as build-debug +WORKDIR /build +COPY --from=planner /build/recipe.json recipe.json +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo --config=.cargo/container_debug.toml \ + chef cook --recipe-path=recipe.json +COPY . . +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo --config=.cargo/container_debug.toml \ + test +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo --config=.cargo/container_debug.toml \ + build + +FROM build as build-release +WORKDIR /build +COPY --from=planner /build/recipe.json recipe.json +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo --config=.cargo/container_release.toml \ + chef cook --release --recipe-path=recipe.json +COPY . . +RUN --mount=type=cache,target=/usr/local/cargo/registry,id=cargo_registry \ + --mount=type=cache,target=/usr/local/rustup,id=rustup_cache \ + cargo --config=.cargo/container_release.toml \ + build --release + +FROM scratch as app +WORKDIR / +VOLUME /adsbot.db +VOLUME /adsbot.log +EXPOSE 3000 + +FROM app as app-debug +WORKDIR / +COPY --from=build-debug /build/target/x86_64-unknown-linux-musl/debug/adsbot /adsbot +ENV RUST_LOG="debug" +ENTRYPOINT [ "/adsbot" ] + +FROM app as app-release +WORKDIR / +COPY --from=build-release /build/target/x86_64-unknown-linux-musl/release/adsbot /adsbot +ENV RUST_LOG="info" +ENTRYPOINT [ "/adsbot" ] diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..7d6a42d --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,52 @@ +[env] +CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true + +[env.development] +PROD = false +DEV = true +_MK_IMAGE_TARGET = 'app-debug' +_MK_IMAGE = 'source.spushk.ru/rustyworks/adsbot:debug' + +[env.production] +PROD = true +DEV = false +_MK_IMAGE_TARGET = 'app-release' +_MK_IMAGE = 'source.spushk.ru/rustyworks/adsbot:v0.1' + +[tasks.rustfmt] +command = "cargo" +args = ["fmt", "--", "--emit=files"] + +[tasks.format] +dependencies = ["rustfmt"] + +[tasks.build] +workspace = false +command = "cargo" +args = ["build"] + +[tasks.image] +workspace = false +condition.env_set = ["_MK_IMAGE_TARGET", "_MK_IMAGE"] +command = "podman" +args = [ + "build", + "--target", "${_MK_IMAGE_TARGET}", + "-t", "${_MK_IMAGE}", + "." +] + +[tasks.image-view] +workspace = false +condition.env_set = ["_MK_IMAGE"] +condition.files_exist = ["${CARGO_MAKE_WORKING_DIRECTORY}/dev-volumes"] +command = "podman" +args = [ + "run", + "--rm", + "-p3000:3000", + "-v./dev-volumes/adsbot.conf:/adsbot.conf", + "-v./dev-volumes/adsbot.log:/adsbot.log", + "-v./dev-volumes/adsbot.db:/adsbot.db", + "${_MK_IMAGE}" +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..51247db --- /dev/null +++ b/README.md @@ -0,0 +1,232 @@ +Very simple bot which will monitor VK ADS form leads and pass them to telegram upstream. Just throwin it here coz friend asked. + +## VK ADS API lead forms + +See the [configuration example](./adsbot.conf.example) + +## Arbitrary POST forms (TBD) + +The arbitrary POST form is created with `adsbot.conf/leadbot/features/formlead.d` TOML +configuration. The format is explained in the example. + +POST request head shall contain `Authorization` header with one of the configured secret values. + +POST request raw body size in bytes is limited by 4KiB. + +Request raw body may be: + +- Either valid UTF-8 JSON string with `Content-Type: application/json` +- Or a messagepack encoded message with `Content-type: application/msgpack` +- Or a raw text, which is reported as-is without parsing with `Content-type: text/plain` + +### Response + +Depending on the request criteria and server state + +### form + +Parsed request body shall contain the field `form`, which is an array limited +by 128 entries. + +Excessive entries shall not be reported, form entries are reported in the order of presense. + +Each of entries must follow the schema: + +```json +{ + type: "object", + "properties": { + "t": { + "type": "string", + "enum": ["text","tel","personal","yn"] + }, + "d": { + "type": "object" + } + } +} +``` + +Any form entry, which does not conform to the specified schema, will not be processed. + +#### `t`/`d` fields + +**"text"** type: + +With the `"t": "text"` field set on the POST data, the `d` field is expected to +follow the schema: + +```json +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "The form question/field name to display in report" + }, + "a": { + "type": "string", + "description": "The user answer/value to display in report" + } + } +} +``` + +The "text" entry shall be included into report as-is. + +**"tel"** type: + +With the `"t": "tel"` field set on the POST data, the `d` field is expected to +follow the schema: + +```json +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "The form question/field name to display in report" + }, + "a": { + "type": "string", + "description": "The value, which will be treated as personal phone number", + } + } +} +``` + +When possible the "tel" entry shall be included into report as `tel:` anchor/link, +and (when possible) shall be hidden under protective block, which does not grant visual access to the phone number without manual interaction or authorization. + +**"personal"** type: + +With the `"t": "personal"` field set on the POST data, the `d` field is expected to +follow the schema: + +```json +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "The form question/field name to display in report" + }, + "a": { + "type": "string", + "description": "The value, which will be treated as personal information" + } + } +} +``` + +When possible shall be hidden under protective block, which does not grant visual access to the phone number without manual interaction or authorization. + +**"yesno"** type: + +With the `"t": "yn"` field set on the POST data, the `d` field is expected to +follow the schema: + +```json +{ + "type": "object", + "properties": { + "q": { + "type": "string", + "description": "The form question/field name to display in report" + }, + "a": { + "type": "boolean", + "description": "The value, which will be treated as yes/no answer" + } + } +} +``` + +#### example + + +The following shall not be parsed and will be reported as-is + +``` +-- HEADERS -- +Authorization: pqo8jdp9:p183ycnpdo1 +Content-Type: text/plain +-- BODY -- +I am a text. I wanna MANY alpacas. I demand: + +- At least 1 black alpaca +- At most 4 white alpacas +- All alpacas to be either black or white +- Exactly 666 alpacas +``` + +The following shall be parsed as JSON + +``` +-- HEADERS -- +Authorization: 0192mu:odump298yfcniudo8c3mo +Content-type: application/json +-- BODY -- +{ + "form": [ + { + "t": "personal", + "d": { + "q": "First name", + "a": "Danis" + } + }, + { + "t": "text", + "d": { + "q": "How much aplacas do you have?", + "a": "120" + } + }, + { + "t": "tel", + "d": { + "q": "Phone", + "a": "88005553535" + } + }, + { + "t": "yn", + "d": { + "q": "Do you want another aplaca?", + "a": true + } + } + ] +} +``` + +## Development + +**Software requirements:** + +- `cargo-make` +- `diesel-cli` +- `podman` + +**Prerequistes:** + +- Create the `dev-volumes/{adsbot.log,adsbot.db,adsbot.conf}` directories, +and configure the `dev-volumes/adsbot.conf` by example provided in `adsbot.conf.example` + +**Altering database schema**: + +Dont forget to create appropriate migrations by + +```sh +diesel migration generate --diff-schema= +``` + +## Building from source + +`cargo make build` for static binary build + +`cargo make image` for container image + +Please, note, that by default on each program startup the license and authors information from Cargo.toml are printed into the log. +If you want to disable it - set the `LOG_CREDENTIALS="none"` env variable during the build diff --git a/adsbot.conf.example/leadbot/adsbot.toml b/adsbot.conf.example/leadbot/adsbot.toml new file mode 100644 index 0000000..a565527 --- /dev/null +++ b/adsbot.conf.example/leadbot/adsbot.toml @@ -0,0 +1,10 @@ +[telegram.bot] +# Telegram bot api token +token = "TOKEN" + +[[telegram.admin]] +user = 111111111 + +[[logging.telegram]] + Chat ID, which will receive logs. For private chat with user - go to the bot and ask him /echo id +chat = 22222222222 diff --git a/adsbot.conf.example/leadbot/features/echo.d/echo.toml b/adsbot.conf.example/leadbot/features/echo.d/echo.toml new file mode 100644 index 0000000..71e2686 --- /dev/null +++ b/adsbot.conf.example/leadbot/features/echo.d/echo.toml @@ -0,0 +1,2 @@ +info="Keep this directory existing to activate echoing features" + diff --git a/adsbot.conf.example/leadbot/features/formlead.d/example.toml b/adsbot.conf.example/leadbot/features/formlead.d/example.toml new file mode 100644 index 0000000..02a7934 --- /dev/null +++ b/adsbot.conf.example/leadbot/features/formlead.d/example.toml @@ -0,0 +1,17 @@ +# Unique (for the formlead feature) identifier. Must be valid URL subpath +id = "unique_id_url_safe" + +[[form.post]] +# The display name for the form report +name = "Display name" +# within the current example POST request is accepted at /unique_id_url_safe/subpath_under_id +path = "subpath_under_id" +# The request sender shall pass "AUTHORIZATION" +# "xqoi83nyra0s98d:4e7b44d9a7b220e2b4bd8ba60085f0aa" +clients = [ + {id="xqoi83nyra0s98d", secret="4e7b44d9a7b220e2b4bd8ba60085f0aa"} +] +# The following destinations will receive form reports +reports = [ + {telegram = {chat = -10000000000000}} +] diff --git a/adsbot.conf.example/leadbot/features/vkadslead.d/example.toml b/adsbot.conf.example/leadbot/features/vkadslead.d/example.toml new file mode 100644 index 0000000..8ec8cad --- /dev/null +++ b/adsbot.conf.example/leadbot/features/vkadslead.d/example.toml @@ -0,0 +1,23 @@ +# The unique (for the vkadslead feature) id. Must be valid URL subpath +id = "unique_id_url_safe" + +[api] +# Client ID received from VK ADS api +client_id = "VKADS API CLIENT ID" +# Client secret receiver from VK ADS api +client_secret = "VKADS API CLIENT SECRET" + +[[telegram.admin]] +user = 111111111 + +[[form]] +# Display name for the form report +name = "Display name" +# Form identifier as shown by VK ADS +id = "VKADS form id" +# I forgot what this does +anonymous = false +# The following destinations shall receive form reports +reports = [ + {telegram = {chat = -10000000000000}} +] diff --git a/diesel.toml b/diesel.toml new file mode 100644 index 0000000..bac45d2 --- /dev/null +++ b/diesel.toml @@ -0,0 +1,13 @@ +# For documentation on how to configure this file, +# see https://diesel.rs/guides/configuring-diesel-cli + +[print_schema.feature_vkadslead] +file = "src/features/vkadslead/db/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[print_schema.integration_telegram] +file = "src/telegram/db/schema.rs" +custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] + +[migrations_directory] +dir = "migrations" diff --git a/migrations/.keep b/migrations/.keep new file mode 100644 index 0000000..e69de29 diff --git a/migrations/2025-09-10-144245_init_feature_vkadslead/down.sql b/migrations/2025-09-10-144245_init_feature_vkadslead/down.sql new file mode 100644 index 0000000..79d820b --- /dev/null +++ b/migrations/2025-09-10-144245_init_feature_vkadslead/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS `features_vkadslead_clientauth`; +DROP TABLE IF EXISTS `features_vkadslead_formsync`; diff --git a/migrations/2025-09-10-144245_init_feature_vkadslead/up.sql b/migrations/2025-09-10-144245_init_feature_vkadslead/up.sql new file mode 100644 index 0000000..8233915 --- /dev/null +++ b/migrations/2025-09-10-144245_init_feature_vkadslead/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS `features_vkadslead_clientauth` ( + `instance` TEXT NOT NULL PRIMARY KEY, + `client_id` TEXT NOT NULL, + `access_token` TEXT NOT NULL, + `ctime` TIMESTAMP NOT NULL, + `expires_in` INTEGER, + `refresh_token` TEXT, + `scope` TEXT, + `token_type` TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS `features_vkadslead_formsync` ( + `instance_id` TEXT NOT NULL, + `form_id` TEXT NOT NULL, + `stime` TIMESTAMP NOT NULL, + PRIMARY KEY (`instance_id`, `form_id`), + FOREIGN KEY (`instance_id`) REFERENCES `features_vkadslead_clientauth` (`instance`) +); diff --git a/migrations/2025-09-10-144305_init_integration_telegram/down.sql b/migrations/2025-09-10-144305_init_integration_telegram/down.sql new file mode 100644 index 0000000..ce91459 --- /dev/null +++ b/migrations/2025-09-10-144305_init_integration_telegram/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS `integrations_telegram_auth`; diff --git a/migrations/2025-09-10-144305_init_integration_telegram/up.sql b/migrations/2025-09-10-144305_init_integration_telegram/up.sql new file mode 100644 index 0000000..83423d2 --- /dev/null +++ b/migrations/2025-09-10-144305_init_integration_telegram/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS `integrations_telegram_auth` ( + `id` INTEGER NOT NULL PRIMARY KEY, + `token` TEXT NOT NULL +); diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..9c8290f --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "nightly-2026-05-10" +components = ["rust-src"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7fc6f04 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,5 @@ +imports_layout="Vertical" +imports_granularity="Module" +group_imports="StdExternalCrate" +merge_imports=true +max_width=120 diff --git a/src/apis/mod.rs b/src/apis/mod.rs new file mode 100644 index 0000000..1086f30 --- /dev/null +++ b/src/apis/mod.rs @@ -0,0 +1 @@ +pub mod vkads; diff --git a/src/apis/vkads/api.rs b/src/apis/vkads/api.rs new file mode 100644 index 0000000..3637789 --- /dev/null +++ b/src/apis/vkads/api.rs @@ -0,0 +1,273 @@ +use oauth2::basic::{ + BasicErrorResponse, + BasicRevocationErrorResponse, + BasicTokenIntrospectionResponse, + BasicTokenType, +}; +use oauth2::{ + AccessToken, + AuthType, + AuthUrl, + ClientId, + ClientSecret, + EndpointNotSet, + EndpointSet, + RefreshToken, + RevocableToken, + RevocationUrl, + Scope, + TokenResponse, + TokenUrl, +}; +use serde::{ + Deserialize, + Serialize, +}; +use tokio::sync::RwLock; + +use super::config::VkadsApiConfig; + +type VkadsOauthClient = oauth2::Client< + BasicErrorResponse, + VkadsapiToken, + BasicTokenIntrospectionResponse, + VkadsapiToken, + BasicRevocationErrorResponse, + EndpointSet, + EndpointNotSet, + EndpointNotSet, + EndpointSet, + EndpointSet, +>; + +pub trait VkadsApiClientAuthState {} + +pub struct Authenticated(RwLock); +impl VkadsApiClientAuthState for Authenticated {} + +pub struct Unauthenticated; +impl VkadsApiClientAuthState for Unauthenticated {} + +pub struct VkadsApiClient { + config: VkadsApiConfig, + api_client: VkadsOauthClient, + api_token: ST, +} + +impl VkadsApiClient { + pub fn new(config: VkadsApiConfig) -> Self { + Self { + api_client: oauth2::Client::new(ClientId::new(config.client_id.clone())) + .set_auth_type(AuthType::RequestBody) + .set_client_secret(ClientSecret::new(config.client_secret.clone())) + .set_auth_uri( + AuthUrl::new("https://ads.vk.com/api/v2/oauth2/token.json".into()) + .expect("Url to be valid. This is a BUG."), + ) + .set_token_uri( + TokenUrl::new("https://ads.vk.com/api/v2/oauth2/token.json".into()) + .expect("Url to be valid. This is a BUG."), + ) + .set_revocation_url( + RevocationUrl::new("https://ads.vk.com/api/v2/oauth2/token/delete.json".into()) + .expect("Url to be valid. This is a BUG."), + ), + config, + api_token: Unauthenticated, + } + } + + /// Try to refresh & use existing token (if has and not expired), + /// or issue new token and write into the provided database + pub async fn authenticated( + self, + existing_auth: Option, + ) -> Result, Box> { + tracing::debug!("Authenticate {} vk api client", &self.config.client_id); + + let http_client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build()?; + + let refresh = existing_auth.as_ref().and_then(|a| a.refresh_token()).cloned(); + + let new_token = match refresh.as_ref() { + Some(refresh) => { + // Try to refresh + tracing::debug!( + "Client {} has existing auth token. Try to refresh", + &self.config.client_id + ); + + let refresh_result = self + .api_client + .exchange_refresh_token(refresh) + .request_async(&http_client) + .await; + + match refresh_result { + Ok(new_token) => { + tracing::debug!("Client {} token refreshed", &self.config.client_id); + new_token + } + Err(e) => { + tracing::debug!( + "Client {} token did not refresh ({e:?}). Require new", + &self.config.client_id + ); + + self.api_client + .exchange_client_credentials() + .request_async(&http_client) + .await? + } + } + } + _ => { + // First time auth - require token + tracing::debug!("Client {} does not have refresh. Require new", &self.config.client_id); + + self.api_client + .exchange_client_credentials() + .request_async(&http_client) + .await? + } + }; + + Ok(VkadsApiClient { + config: self.config, + api_client: self.api_client, + api_token: Authenticated(RwLock::new(new_token)), + }) + } +} + +impl VkadsApiClient { + /// expand reqwest builder with proper VK API authorization header + pub async fn authorize(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder { + let token = self.api_token.0.read().await; + + builder.header("Authorization", token.as_vk_demands()) + } + + /// Refresh token (oauth2 prc) and return new token copy. + /// If the current token does not come with a refresh token, the old token will be returned. + pub async fn refresh(&self) -> Result> { + tracing::debug!("Client {}: refreshing token", &self.config.client_id); + + let mut token = self.api_token.0.write().await; + let Some(refresh_token) = token.refresh_token() else { + return Ok(token.clone()); + }; + + let http_client = reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build()?; + + let new_token = self + .api_client + .exchange_refresh_token(refresh_token) + .request_async(&http_client) + .await?; + + *token = new_token; + + Ok(token.clone()) + } + + pub async fn needs_refresh(&self) -> bool { + let token = self.api_token.0.read().await; + let expiry_time = token + .expires_in + .and_then(|expiry| token.ctime.checked_add_signed(chrono::Duration::seconds(expiry as i64))); + + let before_expiration = expiry_time.map(|expiry| expiry - chrono::Utc::now().naive_utc()); + + before_expiration.is_none_or(|time_left| time_left.num_minutes() < 10) + } + + pub async fn clone_token(&self) -> VkadsapiToken { + self.api_token.0.read().await.clone() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum VkadsapiError { + #[error("Database error")] + DbError(String), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct VkadsapiToken { + pub access_token: AccessToken, + #[serde(deserialize_with = "oauth2::helpers::deserialize_untagged_enum_case_insensitive")] + pub token_type: BasicTokenType, + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_in: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub refresh_token: Option, + #[serde(rename = "scope")] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(default)] + pub scopes: Option>, + #[serde(skip_serializing)] + #[serde(skip_deserializing)] + #[serde(default = "ctime_inplace")] + pub ctime: chrono::NaiveDateTime, +} + +fn ctime_inplace() -> chrono::NaiveDateTime { + chrono::Utc::now().naive_utc() +} + +impl VkadsapiToken { + pub fn as_vk_demands(&self) -> String { + let token_type = match &self.token_type { + BasicTokenType::Bearer => "Bearer", + x => { + tracing::warn!("VK ADS API does not state to support the token type {x:?}"); + "I_DONT_KNOW_WHAT_TO_DO_WITH_IT" + } + }; + format!("{} {}", token_type, self.access_token.secret()) + } +} + +impl RevocableToken for VkadsapiToken { + fn secret(&self) -> &str { + self.access_token.secret() + } + fn type_hint(&self) -> Option<&str> { + Some("access_token") + } +} + +impl TokenResponse for VkadsapiToken { + type TokenType = BasicTokenType; + + fn scopes(&self) -> Option<&Vec> { + self.scopes.as_ref() + } + + fn expires_in(&self) -> Option { + self.expires_in.and_then(|secs| { + if secs >= 0 { + Some(std::time::Duration::from_secs(secs as u64)) + } else { + None + } + }) + } + + fn token_type(&self) -> &Self::TokenType { + &self.token_type + } + + fn access_token(&self) -> &AccessToken { + &self.access_token + } + + fn refresh_token(&self) -> Option<&RefreshToken> { + self.refresh_token.as_ref() + } +} diff --git a/src/apis/vkads/config.rs b/src/apis/vkads/config.rs new file mode 100644 index 0000000..aa4f3d6 --- /dev/null +++ b/src/apis/vkads/config.rs @@ -0,0 +1,16 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsApiConfig { + pub client_id: String, + pub client_secret: String, +} + +impl std::fmt::Display for VkadsApiConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VkadsleadApiConfig") + .field("client_id", &self.client_id) + .field("client_secret", &"[REDACTED]") + .finish() + } +} diff --git a/src/apis/vkads/mappings/contact_info.rs b/src/apis/vkads/mappings/contact_info.rs new file mode 100644 index 0000000..61a1220 --- /dev/null +++ b/src/apis/vkads/mappings/contact_info.rs @@ -0,0 +1,24 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VkadsContactInfo { + pub first_name: Option, + pub email: Option, + pub phone: Option, + pub birth_date: Option, + pub city: Option, + pub social_media_profile: Option, +} + +impl std::fmt::Display for VkadsContactInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VkadsContactInfo") + .field("first_name", &self.first_name) + .field("email", &self.email.as_ref().map(|em| &em[..em.len() / 2])) + .field("phone", &self.phone.as_ref().map(|ph| &ph[..ph.len() / 2])) + .field("birth_date", &"[REDACTED]") + .field("city", &self.city) + .field("social_media_profile", &"[REDACTED]") + .finish() + } +} diff --git a/src/apis/vkads/mappings/lead_answer.rs b/src/apis/vkads/mappings/lead_answer.rs new file mode 100644 index 0000000..0f6cec5 --- /dev/null +++ b/src/apis/vkads/mappings/lead_answer.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +use super::VkadsLeadAnswerOption; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VkadsLeadAnswer { + pub block_id: String, + pub question_text: String, + pub answer_options: Option>, + pub answer_text: Option, +} diff --git a/src/apis/vkads/mappings/lead_answer_option.rs b/src/apis/vkads/mappings/lead_answer_option.rs new file mode 100644 index 0000000..ef0c9e6 --- /dev/null +++ b/src/apis/vkads/mappings/lead_answer_option.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VkadsLeadAnswerOption { + pub id: String, + pub text: String, +} diff --git a/src/apis/vkads/mappings/leads_list_element.rs b/src/apis/vkads/mappings/leads_list_element.rs new file mode 100644 index 0000000..399d897 --- /dev/null +++ b/src/apis/vkads/mappings/leads_list_element.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +use super::{VkadsContactInfo, VkadsLeadAnswer}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VkadsLeadsListElement { + pub id: String, + pub form_id: String, + pub form_name: String, + pub ad_plan_id: u32, + pub ad_group_id: u32, + pub banner_id: u32, + pub created_at: chrono::DateTime, + pub contact_info: VkadsContactInfo, + pub answers: Vec, +} diff --git a/src/apis/vkads/mappings/mod.rs b/src/apis/vkads/mappings/mod.rs new file mode 100644 index 0000000..c568738 --- /dev/null +++ b/src/apis/vkads/mappings/mod.rs @@ -0,0 +1,27 @@ +use serde::{Deserialize, Serialize}; + +mod leads_list_element; +pub use leads_list_element::*; + +mod lead_answer; +pub use lead_answer::*; + +mod contact_info; +pub use contact_info::*; + +mod lead_answer_option; +pub use lead_answer_option::*; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VkadsListResponse { + pub count: Option, + pub offset: Option, + pub limit: Option, + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub enum Cmp { + Lte(T), + Gte(T), +} diff --git a/src/apis/vkads/mod.rs b/src/apis/vkads/mod.rs new file mode 100644 index 0000000..f0d0b08 --- /dev/null +++ b/src/apis/vkads/mod.rs @@ -0,0 +1,19 @@ +use serde::Deserialize; + +pub mod api; +pub mod config; +pub mod mappings; +pub mod request; + +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +pub enum VkadsapiBaseResponse { + Ok(T), + Err { code: String, message: String }, +} + +#[derive(Debug, thiserror::Error)] +pub enum VkadsapiBaseError { + #[error("Response is error ({code}): `{message}`")] + ResponseIsErr { code: String, message: String }, +} diff --git a/src/apis/vkads/request.rs b/src/apis/vkads/request.rs new file mode 100644 index 0000000..970cf52 --- /dev/null +++ b/src/apis/vkads/request.rs @@ -0,0 +1,104 @@ +use super::api::{ + Authenticated, + VkadsApiClient, +}; +use super::mappings::{ + VkadsLeadsListElement, + VkadsListResponse, +}; +use crate::apis::vkads::{ + VkadsapiBaseError, + VkadsapiBaseResponse, +}; +use crate::helpers::formatters::chrono_to_vkapi_datetime; + +#[derive(Default)] +pub struct VkadsRequestLeads { + limit: Option, + offset: Option, + form_id: Option, + sort: Option, + filter_ctime_lte: Option, + filter_ctime_gte: Option, +} + +impl VkadsRequestLeads { + pub fn new() -> Self { + Self::default() + } + + pub async fn fetch( + &self, + http: &reqwest::Client, + api: &VkadsApiClient, + ) -> Result, Box> { + let mut query = vec![]; + if let Some(limit) = &self.limit { + query.push(("limit", limit.to_string())); + } + if let Some(offset) = &self.offset { + query.push(("offset", offset.to_string())); + } + if let Some(form_id) = &self.form_id { + query.push(("_form_ids__in", form_id.clone())); + } + if let Some(sort) = &self.sort { + query.push(("sorting", sort.clone())); + } + if let Some(filter_ctime_gte) = &self.filter_ctime_gte { + query.push(("_created_at__gte", chrono_to_vkapi_datetime(*filter_ctime_gte))); + } + if let Some(filter_ctime_lte) = &self.filter_ctime_lte { + query.push(("_created_at__lte", chrono_to_vkapi_datetime(*filter_ctime_lte))); + } + + let request = http.get("https://ads.vk.com/api/v1/lead_ads/leads.json").query(&query); + let request = api.authorize(request).await.build()?; + + let response = http.execute(request).await?; + + let raw_json = response.text().await?; + let data: VkadsapiBaseResponse> = serde_json::from_str(&raw_json) + .inspect_err(|e| tracing::error!(debug=?(e, raw_json), "While deserializing vkadsapi response"))?; + + match data { + VkadsapiBaseResponse::Ok(data) => Ok(data), + VkadsapiBaseResponse::Err { code, message } => { + Err(Box::new(VkadsapiBaseError::ResponseIsErr { code, message })) + } + } + } + + pub fn limit(self, limit: u32) -> Self { + Self { + limit: Some(limit), + ..self + } + } + + pub fn offset(self, offset: u32) -> Self { + Self { + offset: Some(offset), + ..self + } + } + + pub fn form_id(self, id: String) -> Self { + Self { + form_id: Some(id), + ..self + } + } + + pub fn sort(self, by: String) -> Self { + Self { sort: Some(by), ..self } + } + + pub fn filter_ctime(self, lte: Option, gte: Option) -> Self { + Self { + filter_ctime_lte: lte, + filter_ctime_gte: gte, + ..self + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..d350584 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,22 @@ +use serde::Deserialize; + +use crate::telegram::config::RootTelegramConfig; + +pub const BASE_DIR: &str = "adsbot.conf"; + +#[derive(Clone, Debug, Deserialize)] +pub struct RootConfig { + pub telegram: RootTelegramConfig, + #[serde(default)] + pub logging: LoggingConfig, +} + +#[derive(Default, Debug, Clone, Deserialize)] +pub struct LoggingConfig { + pub telegram: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct LoggingTelegramConfig { + pub chat: i64, +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..cfb67d0 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,44 @@ +use diesel::backend::Backend; +use diesel::connection::SimpleConnection; +use diesel::{ + Connection, + SqliteConnection, +}; +use diesel_migrations::{ + EmbeddedMigrations, + MigrationHarness, + embed_migrations, +}; + +const MIGRATIONS: EmbeddedMigrations = embed_migrations!("migrations"); + +pub fn connect_sqlite_default() -> Result> { + let mut conn = SqliteConnection::establish("adsbot.db/db.sqlite") + .inspect_err(|e| tracing::error!(debug=?e, "Failed to connect sqlite"))?; + conn.batch_execute("PRAGMA foreign_keys = ON;")?; + conn.batch_execute("PRAGMA busy_timeout = 5000;")?; + conn.batch_execute("PRAGMA journal_mode = WAL;")?; + conn.batch_execute("PRAGMA synchronious = NORMAL;")?; + conn.batch_execute("PRAGMA temp_store = MEMORY;")?; + conn.batch_execute("PRAGMA mmap_size = 16777216;")?; + conn.batch_execute("PRAGMA wal_autocheckpoint = 1000;")?; + conn.batch_execute("PRAGMA wal_checkpoint(TRUNCATE);")?; + + let _ = conn + .batch_execute("VACUUM;") + .inspect_err(|e| tracing::warn!(debug=?e, "Sqlite vacuum failed")); + + Ok(conn) +} + +pub fn migrations_up( + conn: &mut impl MigrationHarness, +) -> Result<(), Box> { + tracing::info!("Running pending migrations"); + + conn.run_pending_migrations(MIGRATIONS) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to run pending migrations")) + .inspect(|r| tracing::info!("Migrations: {} up", r.len()))?; + + Ok(()) +} diff --git a/src/features/echo/mod.rs b/src/features/echo/mod.rs new file mode 100644 index 0000000..bffaa60 --- /dev/null +++ b/src/features/echo/mod.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use teloxide::dptree::{ + case, + entry, +}; +use teloxide::macros::BotCommands; +use teloxide::prelude::*; +use tokio::task::JoinSet; + +use super::Feature; +use crate::helpers::formatters::sanitize_telegram_text; + +pub struct EchoFeature {} + +#[async_trait] +impl Feature for EchoFeature { + fn name(&self) -> String { + "echo".into() + } + + fn spawn(&self, _: Arc) -> tokio::task::JoinSet>> { + let mut set = JoinSet::new(); + + set.spawn(async move { + let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(5)); + + loop { + timer.tick().await; + } + }); + + set + } + + fn telegram_tree(&self) -> Option { + let handle_command = entry() + .filter_command::() + .branch( + case![EchoCommand::Echo(text)].endpoint(async |m: Message, bot: Arc, text: String| { + bot.send_message(m.chat.id, text).await?; + Ok::<(), Box>(()) + }), + ) + .branch(case![EchoCommand::Id].endpoint(async |bot: Arc, m: Message| { + let _ = bot + .send_message( + m.chat.id, + format!("`{}`", sanitize_telegram_text(m.chat.id.0.to_string())), + ) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .await + .unwrap(); + + Ok::<(), Box>(()) + })); + + let tree = entry() + .branch(Update::filter_channel_post().chain(handle_command.clone())) + .branch(Update::filter_message().chain(handle_command)); + + Some(tree) + } +} + +#[derive(Clone, BotCommands)] +#[command(rename_rule = "lowercase", description = "Echo feature commands:")] +enum EchoCommand { + #[command(description = "Say this text")] + Echo(String), + #[command(description = "Get channel id")] + Id, +} diff --git a/src/features/formlead/mod.rs b/src/features/formlead/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/features/formlead/mod.rs @@ -0,0 +1 @@ + diff --git a/src/features/helpers/mod.rs b/src/features/helpers/mod.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/features/helpers/mod.rs @@ -0,0 +1 @@ + diff --git a/src/features/mod.rs b/src/features/mod.rs new file mode 100644 index 0000000..7140995 --- /dev/null +++ b/src/features/mod.rs @@ -0,0 +1,406 @@ +use std::path::{ + Path, + PathBuf, +}; +use std::str::FromStr; +use std::sync::Arc; + +use async_trait::async_trait; +use diesel::SqliteConnection; +use teloxide::Bot; +use teloxide::prelude::{ + Requester, + RequesterExt, +}; +use teloxide::types::{ + ChatId, + ParseMode, +}; +use tokio::sync::mpsc::{ + UnboundedReceiver, + UnboundedSender, +}; +use tokio::sync::{ + Mutex, + RwLock, +}; +use tokio::task::JoinSet; +use tracing::field::Visit; +use tracing::{ + Subscriber, + span, +}; +use tracing_subscriber::Layer; +use tracing_subscriber::registry::LookupSpan; + +use crate::config::{ + BASE_DIR, + RootConfig, +}; +use crate::telegram::BotBuilder; +use crate::telegram::tree_root::BotTree; + +pub mod echo; +pub mod formlead; +pub mod helpers; +pub mod vkadslead; + +#[async_trait] +pub trait Feature: Send { + fn name(&self) -> String; + + fn spawn(&self, _bot_clone: Arc) -> JoinSet>> { + let mut set = JoinSet::new(); + set.spawn(async { Ok(()) }); + + set + } + + fn telegram_tree(&self) -> Option { + None + } +} + +pub const BASE_PATH: &str = "features"; + +pub struct FeatureHost { + config: RootConfig, + config_dir: PathBuf, + telegram: RwLock, + features: RwLock>>, + log_rx: RwLock>, +} + +impl FeatureHost { + pub fn new( + base: impl AsRef, + log_rx: UnboundedReceiver, + ) -> Result> { + let config_dir = PathBuf::from_str(BASE_DIR) + .inspect_err(|e| tracing::error!(debug=?e, "BASE_DIR `{BASE_DIR}` is invalid. PANIC.")) + .expect("BASE_DIR to be valid") + .join(base); + + let config_dir = config_dir.canonicalize()?; + + let config_file = config_dir.join("adsbot.toml"); + let config_file = std::fs::read_to_string(&config_file)?; + let config = toml::from_str::(&config_file)?; + + Ok(Self { + config, + config_dir, + features: RwLock::new(vec![]), + telegram: RwLock::new(BotBuilder::new()), + log_rx: RwLock::new(log_rx), + }) + } + + /// Spawn a group of features, which shall read their configs from the base, + /// relative to current host base. + pub async fn spawn_group( + &self, + base: impl AsRef, + spawner: F, + ) -> Result<(), Box> + where + I: Feature + 'static, + F: AsyncFn(&Path) -> Result>, + { + tracing::info!("Spawn group of `{:?}` features", base.as_ref()); + + let base_dir = self.config_dir.join(BASE_PATH).join(base); + + let base_dir = base_dir + .read_dir()? + .filter_map(|f| { + if let Some(simple_path) = f.as_ref().map(|f| f.path()).ok() + && let Ok(canonical_path) = simple_path.canonicalize() + { + if simple_path.extension().is_some_and(|ex| ex == "toml") { + Some(canonical_path) + } else { + tracing::warn!("Skipping {simple_path:?} due to not being .toml file"); + None + } + } else { + tracing::warn!("Skipping {f:?} due to not having canonical path"); + None + } + }) + .collect::>(); + + for conf in &base_dir { + self.insert(spawner(conf).await?).await?; + } + + Ok(()) + } + + /// Insert specific feature, with configuring by path + pub async fn insert(&self, feature: F) -> Result<(), Box> + where + F: Feature + 'static, + { + tracing::debug!("Insert feature `{}`", feature.name()); + + if let Some(telegram_tree) = feature.telegram_tree() { + self.telegram.write().await.branch(telegram_tree) + }; + + self.features.write().await.push(Box::new(feature)); + + Ok(()) + } + + pub async fn run( + self, + telegram_file_db: Arc>, + ) -> Result<(), Box> { + let mut joins = JoinSet::new(); + + tracing::info!("Initialize and run telegram tree"); + let bot = self.telegram.write().await.make_bot(self.config.telegram.clone()); + + tracing::info!("Initialize and run logger task"); + let log_rx = self.log_rx; + let log_bot = bot.clone(); + let log_config = self.config.logging.clone(); + let _ = joins.spawn(async move { + while let Some(HostLoggerMessage { + level, + time, + module, + message, + debug, + }) = log_rx.write().await.recv().await + { + let level_ico = match level { + tracing::Level::TRACE => "", + tracing::Level::DEBUG => "🧐", + tracing::Level::INFO => "📋", + tracing::Level::WARN => "⚠️", + tracing::Level::ERROR => "🔥", + }; + + let tg_text = format!( + "{level_ico} {level}\n{time}\n[{module}]\n\n{message}\n{debug}", + module = module.unwrap_or_else(|| String::from("[no location]")), + time = time.to_rfc2822(), + debug = debug.map(|d| format!("
\n{d}\n
")).unwrap_or_default(), + ); + for tg_dest in &log_config.telegram { + let _ = log_bot + .clone() + .parse_mode(ParseMode::Html) + .send_message(ChatId(tg_dest.chat), &tg_text) + .await + .inspect_err(|e| eprintln!("While duplicating log to tg {}: {e:?}", tg_dest.chat)); + } + } + + tracing::warn!(local = true, "log_rx channel was closed"); + Ok(()) + }); + + let spawnbot = bot.clone(); + let _ = joins.spawn( + self.telegram + .into_inner() + .run(spawnbot, telegram_file_db, self.config.telegram.clone()), + ); + + tracing::info!("Spawn feature server tasks"); + for feature in self.features.read().await.iter() { + let spawnset = feature.spawn(bot.clone()); + let _ = joins.spawn(async move { spawnset.join_all().await.into_iter().collect::>() }); + } + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + tracing::warn!("Ctrl-C signal received. Terminate."); + joins.abort_all(); + Ok(()) + }, + _ = async { + while !joins.is_empty() { + let result = joins.join_next().await; + match result { + Some(Ok(_)) => { + tracing::warn!("Unexpected host task termination with `Ok` result") + }, + Some(Err(e)) => { + tracing::error!("Unexpected host task termination with error: {e:?}") + }, + _ => { + tracing::error!("Unexpected host task termination without result") + } + } + } + } => { + tracing::warn!("There are no tasks left. Terminate"); + Ok(()) + } + } + } +} + +#[derive(Debug, Clone)] +pub struct HostLoggerMessage { + pub level: tracing::Level, + pub time: chrono::DateTime, + pub module: Option, + pub message: String, + pub debug: Option, +} + +#[derive(Default)] +struct EventFields { + /// Set this to true and event shall be ignored + local: Option, + message: Option, + debug: Option, +} + +impl Visit for EventFields { + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "debug" { + self.debug = Some(value.into()) + } + } + + fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { + if field.name() == "local" { + self.local = Some(value) + } + } + + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn core::fmt::Debug) { + match field.name() { + "debug" => self.debug = Some(format!("{value:?}")), + "message" => self.message = Some(format!("{value:?}")), + _ => {} + } + } +} + +#[derive(Default)] +struct SpanFields { + local: Option, +} + +impl SpanFields { + fn has_any_fields(&self) -> bool { + self.local.is_some() + } + + fn under(&mut self, other: &Self) { + other.local.as_ref().map(|v| self.local.replace(*v)); + } + + fn under_owned(&mut self, other: Self) { + other.local.map(|v| self.local.replace(v)); + } +} + +impl Visit for SpanFields { + fn record_bool(&mut self, field: &tracing::field::Field, value: bool) { + if field.name() == "local" { + self.local = Some(value) + } + } + + fn record_debug(&mut self, _field: &tracing::field::Field, _value: &dyn core::fmt::Debug) {} +} + +#[derive(Clone)] +pub struct HostLogger { + sync_tx: UnboundedSender, +} + +impl HostLogger { + pub fn new() -> (Self, UnboundedReceiver) { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel::(); + + (Self { sync_tx: tx }, rx) + } +} + +impl Layer for HostLogger +where + S: Subscriber + for<'a> LookupSpan<'a>, +{ + fn on_new_span(&self, attrs: &span::Attributes<'_>, id: &span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let fields = { + let mut fields = SpanFields::default(); + attrs.record(&mut fields); + fields + }; + + if fields.has_any_fields() + && let Some(span) = ctx.span(id) + { + if let Some(prev) = span.extensions_mut().get_mut::() { + prev.under_owned(fields); + } else { + span.extensions_mut().insert(fields); + } + } + } + + fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) { + // Drop debug from TG + let level = *event.metadata().level(); + if level > tracing::Level::INFO { + return; + } + + let span_fields = match ctx.event_scope(event) { + Some(scope) => { + let mut fields = SpanFields::default(); + for span in scope.from_root() { + if let Some(span_fields) = span.extensions().get::() { + fields.under(span_fields); + } + } + fields + } + None => SpanFields::default(), + }; + + // dont process local span events + if span_fields.local.is_some_and(|v| v) { + return; + } + + let event_fields = { + let mut fields = EventFields::default(); + event.record(&mut fields); + fields + }; + + // also per-event local may be set + if event_fields.local.is_some_and(|v| v) { + return; + } + + let Some(message) = event_fields.message else { + return; + }; + + let message = HostLoggerMessage { + message, + debug: event_fields.debug, + level, + time: chrono::Utc::now(), // fine to me, no need for 100% precision + module: event.metadata().module_path().map(|m| m.into()), + }; + + tracing::info_span!("tx_sending", local = true).in_scope(|| { + _ = self + .sync_tx + .send(message) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to send event to HostLogger tx: {e:?}")); + }); + } +} diff --git a/src/features/vkadslead/config.rs b/src/features/vkadslead/config.rs new file mode 100644 index 0000000..0159c94 --- /dev/null +++ b/src/features/vkadslead/config.rs @@ -0,0 +1,79 @@ +use std::hash::Hasher; +use std::path::Path; + +use serde::Deserialize; + +use crate::apis::vkads::config::VkadsApiConfig; + +#[derive(Debug, Clone)] +pub struct VkadsleadInstance { + pub id: String, + pub config: VkadsleadConfigData, +} + +impl VkadsleadInstance { + pub fn from_toml(file: impl AsRef) -> Result { + let filename_debug = file.as_ref().to_string_lossy().to_string(); + let file_str = std::fs::read_to_string(file.as_ref()) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to read config at {filename_debug}",)) + .map_err(|_| serde::de::Error::custom("Failed to read file"))?; + let config: VkadsleadConfigData = toml::from_str(&file_str)?; + + let id = file + .as_ref() + .file_name() + .map(|mbf| format!("{}", mbf.to_string_lossy())) + .unwrap_or_else(|| { + use std::hash::{ + DefaultHasher, + Hash, + }; + + tracing::warn!( + "ID from file `{}` was not constructed properly. Fallback to client_id only", + file.as_ref().to_string_lossy() + ); + let mut state = DefaultHasher::new(); + config.api.client_id.hash(&mut state); + let client_id_hash = state.finish(); + + format!("{client_id_hash:x}") + }); + Ok(Self { id, config }) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadConfigData { + pub telegram: VkadsleadTelegramConfig, + pub api: VkadsApiConfig, + pub form: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadFormConfig { + pub name: String, + pub id: String, + pub anonymous: bool, + pub reports: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadFormReport { + pub telegram: VkadsleadFormReportTelegram, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadFormReportTelegram { + pub chat: i64, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadTelegramConfig { + pub admin: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct VkadsleadTelegramAdmin { + pub user: u64, +} diff --git a/src/features/vkadslead/db/mod.rs b/src/features/vkadslead/db/mod.rs new file mode 100644 index 0000000..ebbba3d --- /dev/null +++ b/src/features/vkadslead/db/mod.rs @@ -0,0 +1,85 @@ +use diesel::Selectable; +use diesel::prelude::{ + AsChangeset, + Associations, + Identifiable, + Insertable, + Queryable, +}; + +use crate::apis::vkads::api::VkadsapiToken; + +pub mod schema; + +#[derive(Clone, Queryable, Selectable, Insertable, Associations, AsChangeset)] +#[diesel(belongs_to(ClientAuth, foreign_key = instance_id))] +#[diesel(table_name = schema::features_vkadslead_formsync)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct FormSync { + pub instance_id: String, + pub form_id: String, + pub stime: chrono::NaiveDateTime, +} + +#[derive(Clone, Queryable, Selectable, Insertable, Identifiable, AsChangeset)] +#[diesel(primary_key(instance))] +#[diesel(table_name = schema::features_vkadslead_clientauth)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct ClientAuth { + pub instance: String, + pub client_id: String, + pub access_token: String, + pub ctime: chrono::NaiveDateTime, + pub expires_in: Option, + pub refresh_token: Option, + pub scope: Option, + pub token_type: String, +} + +impl ClientAuth { + pub fn from_vkadsapi_token(token: VkadsapiToken, client_id: String, instance: String) -> Self { + Self { + instance, + client_id, + access_token: token.access_token.into_secret(), + expires_in: token.expires_in, + refresh_token: token.refresh_token.map(|rt| rt.into_secret()), + scope: token + .scopes + .map(|t| t.iter().map(|s| s.to_string()).collect::>().join(",")), + token_type: match token.token_type { + oauth2::basic::BasicTokenType::Bearer => "bearer".into(), + d => { + tracing::warn!("Unknown token type: {d:?}, will break"); + "UNKNOWN".into() + } + }, + ctime: token.ctime, + } + } +} + +impl From for VkadsapiToken { + fn from(value: ClientAuth) -> Self { + use oauth2::basic::BasicTokenType; + use oauth2::{ + AccessToken, + RefreshToken, + Scope, + }; + + Self { + access_token: AccessToken::new(value.access_token), + token_type: match value.token_type.to_lowercase().trim() { + "bearer" => BasicTokenType::Bearer, + x => BasicTokenType::Extension(x.to_string()), + }, + expires_in: value.expires_in, + refresh_token: value.refresh_token.map(RefreshToken::new), + scopes: value + .scope + .map(|s| s.split(',').map(|s| Scope::new(s.trim().to_string())).collect()), + ctime: value.ctime, + } + } +} diff --git a/src/features/vkadslead/db/schema.rs b/src/features/vkadslead/db/schema.rs new file mode 100644 index 0000000..8cfce88 --- /dev/null +++ b/src/features/vkadslead/db/schema.rs @@ -0,0 +1,37 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + features_vkadslead_clientauth (instance) { + instance -> Text, + client_id -> Text, + access_token -> Text, + ctime -> Timestamp, + expires_in -> Nullable, + refresh_token -> Nullable, + scope -> Nullable, + token_type -> Text, + } +} + +diesel::table! { + features_vkadslead_formsync (instance_id, form_id) { + instance_id -> Text, + form_id -> Text, + stime -> Timestamp, + } +} + +diesel::table! { + integrations_telegram_auth (id) { + id -> Integer, + token -> Text, + } +} + +diesel::joinable!(features_vkadslead_formsync -> features_vkadslead_clientauth (instance_id)); + +diesel::allow_tables_to_appear_in_same_query!( + features_vkadslead_clientauth, + features_vkadslead_formsync, + integrations_telegram_auth, +); diff --git a/src/features/vkadslead/mod.rs b/src/features/vkadslead/mod.rs new file mode 100644 index 0000000..db1e6cb --- /dev/null +++ b/src/features/vkadslead/mod.rs @@ -0,0 +1,552 @@ +use std::ops::{ + Deref, + DerefMut, +}; +use std::sync::Arc; + +use async_trait::async_trait; +use config::{ + VkadsleadFormConfig, + VkadsleadInstance, +}; +use diesel::{ + OptionalExtension, + QueryDsl, + RunQueryDsl, + SelectableHelper, + SqliteConnection, +}; +use teloxide::Bot; +use teloxide::dispatching::dialogue::GetChatId; +use teloxide::dispatching::{ + HandlerExt, + UpdateFilterExt, +}; +use teloxide::dptree::{ + case, + entry, +}; +use teloxide::macros::BotCommands; +use teloxide::payloads::{ + EditMessageTextSetters, + SendMessageSetters, +}; +use teloxide::prelude::Requester; +use teloxide::types::{ + CallbackQuery, + ChatId, + InlineKeyboardButton, + InlineKeyboardMarkup, + Message, + MessageId, + ParseMode, + Update, +}; +use teloxide::utils::render::RenderMessageTextHelper; +use tokio::sync::{ + Mutex, + RwLock, + mpsc, +}; +use tokio::task::JoinSet; + +use super::Feature; +use crate::apis::vkads::api::{ + Authenticated, + VkadsApiClient, +}; +use crate::apis::vkads::mappings::VkadsLeadsListElement; +use crate::apis::vkads::request::VkadsRequestLeads; +use crate::features::vkadslead::db::{ + ClientAuth, + FormSync, +}; +use crate::helpers::formatters::sanitize_telegram_text; + +pub mod config; +pub mod db; + +#[derive(Clone)] +pub struct VkadsleadFeature { + instance: VkadsleadInstance, + db: Arc>, + api: Arc>>, +} + +impl VkadsleadFeature { + pub async fn init( + instance: VkadsleadInstance, + db: Arc>, + ) -> Result> { + use db::schema::features_vkadslead_clientauth::dsl as clientauth; + + let api = VkadsApiClient::new(instance.config.api.clone()); + let mut db_lock = db.lock().await; + + let restored_auth: Option = clientauth::features_vkadslead_clientauth + .find(&instance.id) + .select(ClientAuth::as_select()) + .first(db_lock.deref_mut()) + .optional()?; + + if restored_auth.is_none() { + tracing::info!("Instance {} was not previously authenticated", instance.id); + } + + let api = api.authenticated(restored_auth.clone().map(|d| d.into())).await?; + + let new_auth = api.clone_token().await; + let new_auth = + ClientAuth::from_vkadsapi_token(new_auth, instance.config.api.client_id.clone(), instance.id.clone()); + + diesel::insert_into(clientauth::features_vkadslead_clientauth) + .values(&new_auth) + .on_conflict(clientauth::instance) + .do_update() + .set(&new_auth) + .execute(db_lock.deref_mut()) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to insert ClientAuth {}", new_auth.instance))?; + + drop(db_lock); + Ok(Self { + instance, + db, + api: Arc::new(RwLock::new(api)), + }) + } +} + +#[async_trait] +impl Feature for VkadsleadFeature { + fn name(&self) -> String { + "vkadslead".into() + } + + fn spawn(&self, bot_clone: Arc) -> tokio::task::JoinSet>> { + use db::schema::features_vkadslead_clientauth::dsl as clientauth; + use db::schema::features_vkadslead_formsync::dsl as formsync; + + let mut set = JoinSet::new(); + let (lead_forward_tx, mut lead_forward_rx) = mpsc::channel::(64); + + let api = self.api.clone(); + let forms = self.instance.config.form.clone(); + let db = self.db.clone(); + let instance_id = self.instance.id.clone(); + let forward_tx = lead_forward_tx.clone(); + + set.spawn(async move { + tracing::debug!("Start periodical lead polling"); + let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(60 * 5)); // 5min + + loop { + timer.tick().await; + let mut form_polls: JoinSet>> = JoinSet::new(); + + for form in &forms { + let form = form.to_owned(); + let fapi = api.clone(); + let instance_id = instance_id.clone(); + let form_forward = forward_tx.clone(); + + let db = db.clone(); + let _ = form_polls.spawn(async move { + tracing::debug!("Poll leads for form `{}`", form.id); + + let leads = VkadsRequestLeads::default() + .form_id(form.id.clone()) + .limit(12) + .sort("created_at".into()); + + let form_sync: Option = formsync::features_vkadslead_formsync + .find((instance_id.clone(), form.id.clone())) + .select(FormSync::as_select()) + .first(db.lock().await.deref_mut()) + .optional() + .inspect_err(|e| { + tracing::error!(debug=?e, "Failed to load form sync for {instance_id}/{id}", id=form.id) + })? + .map(|fs| FormSync { + stime: fs.stime + chrono::Duration::seconds(1), + ..fs + }); + + let leads = if let Some(formsync) = form_sync { + tracing::debug!("Will sync form {instance_id}/{} from {}", &form.id, &formsync.stime); + leads.filter_ctime(None, Some(formsync.stime)) + } else { + leads + }; + + let leads = match leads.fetch(&reqwest::Client::new(), fapi.read().await.deref()).await { + Ok(leads) => Ok(leads), + Err(e) => { + tracing::error!(debug=?e, "Failed to fetch leads for {instance_id}/{id}", id = form.id); + Err(e) + } + }?; + + for fwd in &leads.items { + form_forward + .send(LeadFormForward { + form: form.clone(), + data: fwd.clone(), + }) + .await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to queue lead for forward"))?; + } + + if let Some(last_lead) = leads.items.last() { + let insrt = FormSync { + instance_id, + form_id: form.id, + stime: last_lead.created_at.naive_utc(), + }; + + diesel::insert_into(formsync::features_vkadslead_formsync) + .values(&insrt) + .on_conflict((formsync::instance_id, formsync::form_id)) + .do_update() + .set(&insrt) + .execute(db.lock().await.deref_mut()) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to insert/update new formsync"))?; + } + + Ok(()) + }); + } + + form_polls.join_all().await; + } + }); + + let instance_id = self.instance.id.clone(); + let client_id = self.instance.config.api.client_id.clone(); + let api = self.api.clone(); + let db = self.db.clone(); + + set.spawn(async move { + tracing::debug!("Start periodical access_token refresh"); + let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(30 * 60)); + + loop { + timer.tick().await; + if api.read().await.needs_refresh().await { + tracing::debug!("Instance {instance_id} will refresh access tokens"); + + let new_token = api.write().await.refresh().await?; + let cauth = ClientAuth::from_vkadsapi_token(new_token, client_id.clone(), instance_id.clone()); + diesel::insert_into(clientauth::features_vkadslead_clientauth) + .values(&cauth) + .on_conflict(clientauth::instance) + .do_update() + .set(&cauth) + .execute(db.lock().await.deref_mut()) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to update auth"))?; + } + } + }); + + let bot = bot_clone.clone(); + let forward_tx = lead_forward_tx.clone(); + set.spawn(async move { + tracing::debug!("Start forwarding leads"); + + let messagefn = move |fwd: &LeadFormForward| { + let title = format!("*Лид {}*\n\n", sanitize_telegram_text(&fwd.form.name)); + let person = format!( + "*💁 Имя*: _{}_\n*📞 Телефон*: _{}_\n{}\n", + fwd.data + .contact_info + .first_name + .as_ref() + .map(sanitize_telegram_text) + .unwrap_or("неизвестно".into()), + fwd.data + .contact_info + .phone + .as_ref() + .map(|d| d.replace(|c: char| c != '+' && !c.is_numeric(), "")) + .map(|tel| format!("||[{0}](tel:{0})||", tel.replace('+', r"\+"))) + .unwrap_or("не указан".into()), + fwd.data + .contact_info + .email + .as_ref() + .map(sanitize_telegram_text) + .map(|em| format!("*📜 E-mail*: _[{em}](mailto:{em})_")) + .unwrap_or_default() + ); + let answers = fwd + .data + .answers + .iter() + .filter_map(|answ| match (&answ.answer_text, &answ.answer_options) { + (Some(text), Some(options)) => Some(format!( + "🔹{}\n _{}_:\n{}", + sanitize_telegram_text(&answ.question_text), + options.iter().fold(String::new(), |acc, opt| { + acc + &format!(" _{}_\n", sanitize_telegram_text(&opt.text)) + }), + sanitize_telegram_text(text), + )), + (Some(text), None) => Some(format!( + "🔹{}\n _{}_", + sanitize_telegram_text(&answ.question_text), + sanitize_telegram_text(text) + )), + (None, Some(options)) => Some(options.iter().fold(String::new(), |acc, opt| { + acc + &format!( + "🔹{}\n _{}_\n", + sanitize_telegram_text(&answ.question_text), + sanitize_telegram_text(&opt.text) + ) + })), + _ => None, + }) + .fold(String::new(), |acc, next| acc + &next + "\n"); + + format!("{title}\n{person}\n{answers}") + }; + + let keyboard = InlineKeyboardMarkup::new([[ + InlineKeyboardButton::callback("👍 Готов", "vkadslead::lead::ok"), + InlineKeyboardButton::callback("💢 Отказать", "vkadslead::lead::cancel"), + ]]); + + // telegram faq: + // "In a group, bots are not be able to send more than 20 messages per minute." + // we're going `a bit safer` per feature + let mut timer = tokio::time::interval(tokio::time::Duration::from_secs(5)); + while let Some(fwd) = lead_forward_rx.recv().await { + for report in &fwd.form.reports { + match bot + .send_message(ChatId(report.telegram.chat), messagefn(&fwd)) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .reply_markup(keyboard.clone()) + .await + { + Ok(_) => {} + Err(e) => { + tracing::error!( + debug=?e, + "Forward lead `{}` (form `{}`) to chat `{}` will queue again", + &fwd.data.id, + &fwd.form.id, + report.telegram.chat + ); + let _ = forward_tx + .send(fwd.clone()) + .await + .inspect_err(|e| tracing::error!(debug=?e, "While queuing retry lead")); + } + } + } + timer.tick().await; + } + + tracing::warn!("Telegram lead forward is stopped due to channel close"); + Ok(()) + }); + + set + } + + fn telegram_tree(&self) -> Option { + let instance = Arc::new(self.instance.clone()); + + Some( + entry() + .branch( + Update::filter_callback_query() + .filter_map(|q: CallbackQuery| q.chat_id()) + .filter_map(|q: CallbackQuery| { + q.message.and_then(|m| m.regular_message().cloned()) + }) + .branch( + entry() + .filter(|q: CallbackQuery| { + q.data.is_some_and(|d| d == "vkadslead::lead::ok") + }) + .endpoint( + async move |bot: Arc, + q: CallbackQuery, + chat: ChatId, + msg: Message| { + let text = q + .message + .as_ref() + .and_then(|m| m.regular_message()) + .and_then(|m| m.markdown_text()) + .map(|t| t.to_string()) + .unwrap_or_default(); + let text = format!( + "{text}\n\n> 👍 Обработан: {}", + q.from.first_name + ); + + lead_callback_editor(bot, chat, msg.id, text).await; + + Ok(()) + }, + ), + ) + .branch( + entry() + .filter(|q: CallbackQuery| { + q.data.is_some_and(|d| d == "vkadslead::lead::cancel") + }) + .endpoint( + async move |bot: Arc, + q: CallbackQuery, + chat: ChatId, + msg: Message| { + let text = q + .message + .as_ref() + .and_then(|m| m.regular_message()) + .and_then(|m| m.markdown_text()) + .map(|t| t.to_string()) + .unwrap_or_default(); + let text = format!( + "{text}\n\n> 💢 Отменён: {}", + q.from.first_name + ); + + lead_callback_editor(bot, chat, msg.id, text).await; + + Ok(()) + }, + ), + ), + ) + .branch( + Update::filter_message() + .map(move || instance.clone()) + .filter_command::() + .filter(move |instance: Arc, msg: Message| { + if let Some(from_id) = msg.from.as_ref().map(|mf| mf.id.0) { + instance.config.telegram.admin.iter() + .map(|a| a.user) + .any(|admin_id| + admin_id == from_id + ) + } else { + false + } + }) + .branch( + case![AdminCommand::VkadsFormSyncSet { + id, + form_id, + stime_ymd, + }] + .filter( + move |instance: Arc, (id, form_id, _stime_ymd): (String, String, String)| { + id == instance.id && + instance.config.form + .iter() + .any(|f| f.id == form_id) + }, + ) + .filter_map_async( + async |bot: Arc, msg: Message, (id, form_id, stime_ymd): (String, String, String)| { + let ymd_format = "%Y-%m-%d"; + + let try_naive = + chrono::NaiveDate::parse_from_str(&stime_ymd, ymd_format).inspect_err(|e| { + tracing::warn!(debug=?e, "Failed to parse command VkadsFormSyncSet for {id}/{form_id} datetime {stime_ymd}"); + }); + match try_naive { + Ok(stime) => { + let stime = chrono::NaiveDateTime::from(stime); + + Some(FormSync { + instance_id: id, + form_id, + stime + }) + }, + Err(_) => { + let _ = bot.send_message(msg.chat.id, + format!("Invalid format:\n❌ Was `{}`\n➡️ Must be `{}`\n✅ For example `{}`", sanitize_telegram_text(stime_ymd), sanitize_telegram_text(ymd_format), sanitize_telegram_text("2025-01-01")) + ).parse_mode(ParseMode::MarkdownV2).await; + + None + } + } + }, + ) + .endpoint(async |bot: Arc, msg: Message, db: Arc>, formsync: FormSync| { + use db::schema::features_vkadslead_formsync::dsl; + + diesel::insert_into(dsl::features_vkadslead_formsync) + .values(&formsync) + .on_conflict((dsl::instance_id, dsl::form_id)) + .do_update() + .set(&formsync) + .execute(db.lock().await.deref_mut()) + .inspect_err(|e| { + tracing::error!(debug=?e, "Failed to insert/update new formsync") + })?; + + let reply = format!("✅ Form {}/{} replays {}", formsync.instance_id, formsync.form_id, formsync.stime); + + let _ = bot.send_message(msg.chat.id, reply) + .await; + + Ok(()) + }), + ) + .branch(case![AdminCommand::VkadsForms {id}].filter(move |instance: Arc, id: String| { + id == instance.id + }).endpoint(async |bot: Arc, instance: Arc, msg: Message| { + let configured_forms = instance.config.form + .iter() + .map(|f|{ + let reports = f.reports.iter().map(|rp| format!("[TG: {}]", rp.telegram.chat)).collect::>().join(","); + + format!("{}:\nID: {}\n->: {reports}", f.name, f.id) + }) + .collect::>().join("\n\n"); + + let reply = format!("{} forms:\n\n{configured_forms}", instance.id); + + let _ = bot.send_message(msg.chat.id, reply).await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to send message")); + Ok(()) + })) + ), + ) + } +} + +#[derive(BotCommands, Clone)] +#[command(rename_rule = "lowercase", description = "The commands for vkadslead feature:")] +enum AdminCommand { + #[command( + description = "set instance/form sync date. Syntax: `/vkadsformsyncset {instance} {form} YYYY-MM-DD HH:MM:SS`", + parse_with = "split" + )] + VkadsFormSyncSet { + id: String, + form_id: String, + stime_ymd: String, + }, + #[command(description = "show configured forms for instance. Syntax: `/vkadsforms {instance}`")] + VkadsForms { id: String }, +} + +#[derive(Clone)] +struct LeadFormForward { + form: VkadsleadFormConfig, + data: VkadsLeadsListElement, +} + +async fn lead_callback_editor(bot: Arc, chat: ChatId, msg: MessageId, text: String) { + let _ = bot + .edit_message_text(chat, msg, text) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to edit lead text by callback")); +} diff --git a/src/helpers/formatters.rs b/src/helpers/formatters.rs new file mode 100644 index 0000000..b6747e3 --- /dev/null +++ b/src/helpers/formatters.rs @@ -0,0 +1,37 @@ +use std::sync::LazyLock; + +use chrono::{ + Datelike, + Timelike, +}; +use regex::Regex; + +/// the `datetime` is expected to represent datetime at UTC +pub fn chrono_to_vkapi_datetime(datetime: chrono::NaiveDateTime) -> String { + let foff = datetime + .and_utc() + .with_timezone(&chrono::FixedOffset::east_opt(3 * 3600).unwrap()) // MSK + .fixed_offset(); + + format!( + "{:04}-{:02}-{:02} {:02}:{:02}:{:02}", + foff.year(), + foff.month(), + foff.day(), + foff.hour(), + foff.minute(), + foff.second() + ) +} + +static SANITIZE_TG_REG: LazyLock = LazyLock::new(|| { + const REG: &str = r"[\\_*\[\]\(\)\{\}~`><&#=|.!+-]"; + Regex::new(REG) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to construct regexp {REG}")) + .expect("To construct relegram escape regexp") +}); + +pub fn sanitize_telegram_text(text: impl AsRef) -> String { + let regx = SANITIZE_TG_REG.clone(); + regx.replace_all(text.as_ref(), r"\$0").to_string() +} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs new file mode 100644 index 0000000..06990ca --- /dev/null +++ b/src/helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod formatters; +pub mod reqwest_pool; +pub mod serializers; diff --git a/src/helpers/reqwest_pool.rs b/src/helpers/reqwest_pool.rs new file mode 100644 index 0000000..4c3ac63 --- /dev/null +++ b/src/helpers/reqwest_pool.rs @@ -0,0 +1,24 @@ +use deadpool::managed::{Manager, Pool}; + +pub type ReqwestClientPool = Pool; + +pub struct ReqwestClientPoolManager {} + +impl Manager for ReqwestClientPoolManager { + type Type = reqwest::Client; + type Error = reqwest::Error; + + async fn create(&self) -> Result { + reqwest::ClientBuilder::new() + .redirect(reqwest::redirect::Policy::none()) + .build() + } + + async fn recycle( + &self, + _obj: &mut Self::Type, + _metrics: &deadpool::managed::Metrics, + ) -> deadpool::managed::RecycleResult { + Ok(()) + } +} diff --git a/src/helpers/serializers.rs b/src/helpers/serializers.rs new file mode 100644 index 0000000..b8765e1 --- /dev/null +++ b/src/helpers/serializers.rs @@ -0,0 +1,18 @@ +use serde::Serializer; + +pub fn serialize_comma_delimited_vec( + vec_opt: &Option>, + serializer: S, +) -> Result +where + T: AsRef, + S: Serializer, +{ + if let Some(ref vec) = *vec_opt { + let space_delimited = vec.iter().map(|s| s.as_ref()).collect::>().join(","); + + serializer.serialize_str(&space_delimited) + } else { + serializer.serialize_none() + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b775aca --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +pub mod apis; +pub mod config; +pub mod db; +pub mod features; +pub mod helpers; +pub mod telegram; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..234e99b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,69 @@ +use std::sync::Arc; + +use adsbot::db::{ + connect_sqlite_default, + migrations_up, +}; +use adsbot::features::echo::EchoFeature; +use adsbot::features::vkadslead::VkadsleadFeature; +use adsbot::features::vkadslead::config::VkadsleadInstance; +use adsbot::features::{ + FeatureHost, + HostLogger, +}; +use tokio::sync::Mutex; +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +#[tokio::main] +async fn main() { + let (host_logger_layer, host_logger_rx) = HostLogger::new(); + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer()) + .with(host_logger_layer) + .init(); + + let credentials = { + if option_env!("LOG_CREDENTIALS").is_some_and(|v| v == "none") { + "" + } else { + const_str::concat!( + "This software was built with ❤️ by ", + env!("CARGO_PKG_AUTHORS"), + " under terms of ", + env!("CARGO_PKG_LICENSE"), + " license" + ) + } + }; + tracing::info!("{credentials}"); + + let host = FeatureHost::new("leadbot", host_logger_rx).unwrap(); + + let mut db = connect_sqlite_default() + .inspect_err(|e| tracing::error!(debug=?e, "Failed to create connection. PANIC.")) + .expect("To connect sqlite"); + migrations_up(&mut db) + .inspect_err(|e| tracing::error!(debug=?e, "Failed to run migrations")) + .expect("To migrate"); + + let db = Arc::new(Mutex::new(db)); + + _ = host + .spawn_group("vkadslead.d", async |config_path| { + let instance = VkadsleadInstance::from_toml(config_path)?; + VkadsleadFeature::init(instance, db.clone()).await + }) + .await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to spawn host group `vkadslead`")); + + _ = host + .spawn_group("echo.d", async |_| Ok(EchoFeature {})) + .await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to spawn host group `echo`")); + + host.run(db.clone()) + .await + .inspect_err(|e| tracing::error!(debug=?e, "Failed to run host")) + .unwrap(); +} diff --git a/src/telegram/config.rs b/src/telegram/config.rs new file mode 100644 index 0000000..ecd7abc --- /dev/null +++ b/src/telegram/config.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct RootTelegramConfig { + pub bot: RootTelegramBot, + #[serde(default)] + pub admin: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RootTelegramBot { + pub token: String, +} + +impl std::fmt::Display for RootTelegramBot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RootTelegramBot").field("token", &"[REDACTED]").finish() + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct RootTelegramAdmin { + pub user: i64, +} diff --git a/src/telegram/db/mod.rs b/src/telegram/db/mod.rs new file mode 100644 index 0000000..1ce7e17 --- /dev/null +++ b/src/telegram/db/mod.rs @@ -0,0 +1 @@ +pub mod schema; diff --git a/src/telegram/db/schema.rs b/src/telegram/db/schema.rs new file mode 100644 index 0000000..8cfce88 --- /dev/null +++ b/src/telegram/db/schema.rs @@ -0,0 +1,37 @@ +// @generated automatically by Diesel CLI. + +diesel::table! { + features_vkadslead_clientauth (instance) { + instance -> Text, + client_id -> Text, + access_token -> Text, + ctime -> Timestamp, + expires_in -> Nullable, + refresh_token -> Nullable, + scope -> Nullable, + token_type -> Text, + } +} + +diesel::table! { + features_vkadslead_formsync (instance_id, form_id) { + instance_id -> Text, + form_id -> Text, + stime -> Timestamp, + } +} + +diesel::table! { + integrations_telegram_auth (id) { + id -> Integer, + token -> Text, + } +} + +diesel::joinable!(features_vkadslead_formsync -> features_vkadslead_clientauth (instance_id)); + +diesel::allow_tables_to_appear_in_same_query!( + features_vkadslead_clientauth, + features_vkadslead_formsync, + integrations_telegram_auth, +); diff --git a/src/telegram/mod.rs b/src/telegram/mod.rs new file mode 100644 index 0000000..7e62585 --- /dev/null +++ b/src/telegram/mod.rs @@ -0,0 +1,5 @@ +pub mod config; +pub mod db; +pub mod tree_root; + +pub use tree_root::BotBuilder; diff --git a/src/telegram/tree_root.rs b/src/telegram/tree_root.rs new file mode 100644 index 0000000..28864da --- /dev/null +++ b/src/telegram/tree_root.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; +use std::time::Duration; + +use diesel::SqliteConnection; +use teloxide::Bot; +use teloxide::dispatching::DpHandlerDescription; +use teloxide::dptree::{ + Handler, + deps, + entry, +}; +use teloxide::prelude::Dispatcher; +use tokio::sync::Mutex; + +use super::config::RootTelegramConfig; + +pub type BotTree = Handler< + 'static, + //DependencyMap, + Result<(), Box>, + DpHandlerDescription, +>; + +pub struct BotBuilder { + tree: BotTree, +} + +impl BotBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn make_bot(&self, config: RootTelegramConfig) -> Arc { + let http_client = reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_secs(30)) + .timeout(Duration::from_secs(24)) + .tcp_nodelay(true) + .build() + .inspect_err(|e| tracing::error!(debug=?e, "Failed to build http_client")) + .expect("To build HTTP client"); + + Arc::new(Bot::with_client(config.bot.token.clone(), http_client)) + } + + pub async fn run( + self, + bot: Arc, + db: Arc>, + config: RootTelegramConfig, + ) -> Result<(), Box> { + let Self { tree } = self; + + Dispatcher::builder(bot, tree) + .dependencies(deps![db, config]) + .enable_ctrlc_handler() + .build() + .dispatch() + .await; + + Ok(()) + } +} + +impl BotBuilder { + pub fn branch(&mut self, handler: BotTree) { + self.tree = self.tree.clone().branch(handler) + } +} + +impl Default for BotBuilder { + fn default() -> Self { + Self { tree: entry() } + } +}