From ea042c33ba1723b122cbf2a799505755dab871aa Mon Sep 17 00:00:00 2001 From: Artyom Belousov Date: Sun, 8 Feb 2026 01:01:17 +0300 Subject: [PATCH] Initial commit --- .dockerignore | 2 + .env.template | 18 + .gitignore | 3 + Cargo.lock | 2354 +++++++++++++++++++++++++++ Cargo.toml | 17 + Dockerfile | 21 + LICENSE | 15 + Logo.jpg | Bin 0 -> 81284 bytes README.md | 16 + README_RU.md | 16 + compose.yaml | 8 + devbox.json | 20 + devbox.lock | 294 ++++ src/application.rs | 317 ++++ src/domain.rs | 63 + src/infrastructure.rs | 5 + src/infrastructure/query.rs | 51 + src/infrastructure/scryfall.rs | 160 ++ src/infrastructure/starcitygames.rs | 209 +++ src/infrastructure/telegram.rs | 178 ++ src/infrastructure/vk.rs | 144 ++ src/lib.rs | 4 + src/main.rs | 191 +++ src/presentation.rs | 2 + src/presentation/telegram.rs | 182 +++ src/presentation/vk.rs | 162 ++ 26 files changed, 4452 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.template create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 Logo.jpg create mode 100644 README.md create mode 100644 README_RU.md create mode 100644 compose.yaml create mode 100644 devbox.json create mode 100644 devbox.lock create mode 100644 src/application.rs create mode 100644 src/domain.rs create mode 100644 src/infrastructure.rs create mode 100644 src/infrastructure/query.rs create mode 100644 src/infrastructure/scryfall.rs create mode 100644 src/infrastructure/starcitygames.rs create mode 100644 src/infrastructure/telegram.rs create mode 100644 src/infrastructure/vk.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/presentation.rs create mode 100644 src/presentation/telegram.rs create mode 100644 src/presentation/vk.rs diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f83806 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/target +/.env diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..8094b70 --- /dev/null +++ b/.env.template @@ -0,0 +1,18 @@ +TELEGRAM_API_URL=https://api.telegram.org +TELEGRAM_SECRET= +TELEGRAM_TOKEN= +TELEGRAM_CALLBACK_URL= + +VK_API_URL=https://api.vk.ru +VK_TOKEN= +VK_SECRET= +VK_CONFIRMATION_CODE= + +SCG_SEARCH_URL=https://starcitygamesv2.searchapi-na.hawksearch.com +SCG_CLIENT_GUID=cc3be22005ef47d3969c3de28f09571b +SCG_DISPLAY_URL=https://starcitygames.com +SCG_MAX_OFFERS=5 + +SCRYFALL_URL=https://api.scryfall.com + +BIND_ADDRESS=0.0.0.0:3000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1c71cd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/.env +/.envrc diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..3d00d5b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2354 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7b6141e96a8c160799cc2d5adecd5cbbe5054cb8c7c4af53da0f83bb7ad256" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" + +[[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.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[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 = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[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 = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "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" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.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 = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[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", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[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 = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[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", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.180" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[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 = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58d964098a5f9c6b63d0798e5372fd04708193510a7af313c22e9f29b7b620b" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca41ce716dda6a9be188b385aa78ee5260fc25cd3802cb2a8afdc6afbe6b6dbf" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "mockito" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90820618712cab19cfc46b274c6c22546a82affcb3c3bdf0f29e3db8e1bb92c0" +dependencies = [ + "assert-json-diff", + "bytes", + "colored", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "log", + "pin-project-lite", + "rand", + "regex", + "serde_json", + "serde_urlencoded", + "similar", + "tokio", +] + +[[package]] +name = "mtg-price-bot" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "mockall", + "mockito", + "reqwest", + "rstest", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[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 = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[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-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + +[[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 = "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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "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.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[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 = "rstest" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" +dependencies = [ + "futures-timer", + "futures-util", + "rstest_macros", +] + +[[package]] +name = "rstest_macros" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" +dependencies = [ + "cfg-if", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version", + "syn", + "unicode-ident", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.23.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[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_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 = "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 = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[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.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +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 = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[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 = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +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.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +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-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_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_edit" +version = "0.23.10+spec-1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "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", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +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 = [ + "log", + "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.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "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 = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[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", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +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-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[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.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[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.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..69a96e0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mtg-price-bot" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.101" +axum = "0.8.8" +mockall = "0.14.0" +mockito = "1.7.2" +reqwest = { version = "0.13.2", features=["json"] } +rstest = "0.26.1" +serde = { version = "1.0.228", features=["derive"] } +serde_json = "1.0.149" +tokio = { version = "1.49.0", features=["full"] } +tracing = "0.1.44" +tracing-subscriber = "0.3.22" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a0d5459 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef +WORKDIR /app + +FROM chef AS planner +COPY . . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +# Build dependencies - this is the caching Docker layer! +RUN cargo chef cook --release --recipe-path recipe.json +# Build application +COPY . . +RUN cargo build --release --bin mtg-price-bot + +# We do not need the Rust toolchain to run the binary! +FROM debian:trixie-slim AS runtime +WORKDIR /app +COPY --from=builder /app/target/release/mtg-price-bot /usr/local/bin +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +ENTRYPOINT ["/usr/local/bin/mtg-price-bot"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6cead53 --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +BSD Zero Clause License + +Copyright (c) 2026 Artyom Belousov + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + diff --git a/Logo.jpg b/Logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e61820d9b16b292bf4e67829e15378add6298dfb GIT binary patch literal 81284 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%0A)7T<0_G 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 I>i^#a0GK{=T>t<8 literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bcd1328 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# MTG price bot +EN | [RU](https://codeberg.org/flygrounder/mtg-price-bot/src/branch/main/README_RU.md) + +![Logo](/Logo.jpg) + +VK bot: https://vk.ru/mtg_vk_bot +Telegram bot: https://t.me/MtgScgBot + +Search prices for MTG cards from http://www.starcitygames.com/ without leaving your favourite messenger. +No need to learn bot commands, just send it a card name in any language and it will find prices for different printings of that card. + +To find cards even more quickly, use command +!s XXX YYY +XXX - set number +YYY - collectors number +You can find them in the bottom left corner of a card. diff --git a/README_RU.md b/README_RU.md new file mode 100644 index 0000000..bfb11f8 --- /dev/null +++ b/README_RU.md @@ -0,0 +1,16 @@ +# MTG price bot +[EN](https://codeberg.org/flygrounder/mtg-price-bot/src/branch/main/README.md) | RU + +![Logo](/Logo.jpg) + +Бот в ВК: https://vk.ru/mtg_vk_bot +Бот в Telegram: https://t.me/MtgScgBot + +Ищите цены на карты MTG с сайта http://www.starcitygames.com/ не выходя из вашего любимого мессенджера. +Не нужно учить никаких команд, просто отправьте боту название карты на любом языке и он предложит вам цены на разные издания этой карты. + +Если же вы хотите искать карты ещё быстрее, то воспользуйтесь командой +!s XXX YYY +XXX - короткий код издания +YYY - коллекционный номер карты +Они расположены в нижнем левом углу карты diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..286ee67 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + app: + restart: unless-stopped + build: . + env_file: + - .env + ports: + - 127.0.0.1:3000:3000 diff --git a/devbox.json b/devbox.json new file mode 100644 index 0000000..868debb --- /dev/null +++ b/devbox.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json", + "packages": [ + "cargo@latest", + "rustc@latest", + "bacon@latest", + "rustfmt@latest", + "clippy@latest" + ], + "shell": { + "init_hook": [ + "echo 'Welcome to devbox!' > /dev/null" + ], + "scripts": { + "test": [ + "echo \"Error: no test specified\" && exit 1" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 0000000..73d5782 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,294 @@ +{ + "lockfile_version": "1", + "packages": { + "bacon@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#bacon", + "source": "devbox-search", + "version": "3.22.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/d1ikw8xrns7cbfqypwcgxcchwzvdq7cg-bacon-3.22.0", + "default": true + } + ], + "store_path": "/nix/store/d1ikw8xrns7cbfqypwcgxcchwzvdq7cg-bacon-3.22.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/kmhjc2p8hxf3hkdja6cqi0khqda5jxv4-bacon-3.22.0", + "default": true + } + ], + "store_path": "/nix/store/kmhjc2p8hxf3hkdja6cqi0khqda5jxv4-bacon-3.22.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/5ymilhncmbc9iw2il7zj5wzpv7alnhd8-bacon-3.22.0", + "default": true + } + ], + "store_path": "/nix/store/5ymilhncmbc9iw2il7zj5wzpv7alnhd8-bacon-3.22.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/19ca86jyjzy2r6kppssjc0y9r8knxcpf-bacon-3.22.0", + "default": true + } + ], + "store_path": "/nix/store/19ca86jyjzy2r6kppssjc0y9r8knxcpf-bacon-3.22.0" + } + } + }, + "cargo@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#cargo", + "source": "devbox-search", + "version": "1.92.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/v4bvnkm0p5x41fhybskr0cf2zvkgyrvv-cargo-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/v4bvnkm0p5x41fhybskr0cf2zvkgyrvv-cargo-1.92.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/piiqs6x2m8gv0n3z3pys8scn0y673piy-cargo-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/piiqs6x2m8gv0n3z3pys8scn0y673piy-cargo-1.92.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wf6279pgydd1nny4s5nx1msian6dbf9p-cargo-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/wf6279pgydd1nny4s5nx1msian6dbf9p-cargo-1.92.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/yqcsaywfvcyy9wmbzb5fawp29icgi7cb-cargo-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/yqcsaywfvcyy9wmbzb5fawp29icgi7cb-cargo-1.92.0" + } + } + }, + "clippy@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#clippy", + "source": "devbox-search", + "version": "1.92.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/phhksxd0vv7ml9imsr0lwiqvvmiaz23p-clippy-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/phhksxd0vv7ml9imsr0lwiqvvmiaz23p-clippy-1.92.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/1iqky89dxzj12yy9dsbyrarknswx6iyj-clippy-1.92.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/anj7w4nnbw9cvsrsq85vnwqlw8miss5n-clippy-1.92.0-debug" + } + ], + "store_path": "/nix/store/1iqky89dxzj12yy9dsbyrarknswx6iyj-clippy-1.92.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/34yqzpcjb1cms86lij7wkszcjxdivx6f-clippy-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/34yqzpcjb1cms86lij7wkszcjxdivx6f-clippy-1.92.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zmrbcbd77w6nylgwyagnxh87all5swjf-clippy-1.92.0", + "default": true + }, + { + "name": "debug", + "path": "/nix/store/yvamzvy4r4ml91sl0fap3jvp12wgwflx-clippy-1.92.0-debug" + } + ], + "store_path": "/nix/store/zmrbcbd77w6nylgwyagnxh87all5swjf-clippy-1.92.0" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-01-30T02:32:49Z", + "resolved": "github:NixOS/nixpkgs/6308c3b21396534d8aaeac46179c14c439a89b8a?lastModified=1769740369&narHash=sha256-xKPyJoMoXfXpDM5DFDZDsi9PHArf2k5BJjvReYXoFpM%3D" + }, + "rustc@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "plugin_version": "0.0.1", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#rustc", + "source": "devbox-search", + "version": "1.92.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/ymskl36napcfgl6wjz1xdjn0jd25inrv-rustc-wrapper-1.92.0", + "default": true + }, + { + "name": "man", + "path": "/nix/store/ch54xfkz0dlqvhbinzlbkva2898nvihl-rustc-wrapper-1.92.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/09yhz82jqxwbmn4dbjy7p9hrvbr4g0mn-rustc-wrapper-1.92.0-doc" + } + ], + "store_path": "/nix/store/ymskl36napcfgl6wjz1xdjn0jd25inrv-rustc-wrapper-1.92.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qnvqgfiqh8s08cqp452665l2b60a811h-rustc-wrapper-1.92.0", + "default": true + }, + { + "name": "man", + "path": "/nix/store/5ggpm5m3wkxj05si2id4b6sq4alf90qg-rustc-wrapper-1.92.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/camfpqmi94ssc1p8vkygp2s621ykq80m-rustc-wrapper-1.92.0-doc" + } + ], + "store_path": "/nix/store/qnvqgfiqh8s08cqp452665l2b60a811h-rustc-wrapper-1.92.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/zkcwgmgli06nsh0v8yv82gnh5whgcdyl-rustc-wrapper-1.92.0", + "default": true + }, + { + "name": "man", + "path": "/nix/store/2k3fdy9xjwkx06rlfwfn449w442vgk0i-rustc-wrapper-1.92.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/7271gqq93bbxrqb9rw3hf5b5x6wdm2cn-rustc-wrapper-1.92.0-doc" + } + ], + "store_path": "/nix/store/zkcwgmgli06nsh0v8yv82gnh5whgcdyl-rustc-wrapper-1.92.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/qvpg842zrjkywv7sqgw2h05spdyzcj86-rustc-wrapper-1.92.0", + "default": true + }, + { + "name": "man", + "path": "/nix/store/n1d4093lcx7ljgks1j352fbrf5w551v9-rustc-wrapper-1.92.0-man", + "default": true + }, + { + "name": "doc", + "path": "/nix/store/425gvb2xnhkwb1izjr97wpwdwndwi516-rustc-wrapper-1.92.0-doc" + } + ], + "store_path": "/nix/store/qvpg842zrjkywv7sqgw2h05spdyzcj86-rustc-wrapper-1.92.0" + } + } + }, + "rustfmt@latest": { + "last_modified": "2026-01-23T17:20:52Z", + "resolved": "github:NixOS/nixpkgs/a1bab9e494f5f4939442a57a58d0449a109593fe#rustfmt", + "source": "devbox-search", + "version": "1.92.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/max1hp93q51kfj074lw6lg8w9m5nmh10-rustfmt-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/max1hp93q51kfj074lw6lg8w9m5nmh10-rustfmt-1.92.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/vq6cxhd7iv2g444imys9wkmwqz3ffqbc-rustfmt-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/vq6cxhd7iv2g444imys9wkmwqz3ffqbc-rustfmt-1.92.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/dnjwscz1r33dkmikx3ms58qmhr60db9i-rustfmt-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/dnjwscz1r33dkmikx3ms58qmhr60db9i-rustfmt-1.92.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gl4y2v4lwiyllh85z712w8ajydia053l-rustfmt-1.92.0", + "default": true + } + ], + "store_path": "/nix/store/gl4y2v4lwiyllh85z712w8ajydia053l-rustfmt-1.92.0" + } + } + } + } +} diff --git a/src/application.rs b/src/application.rs new file mode 100644 index 0000000..bad7e42 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,317 @@ +use anyhow::Context; +use anyhow::Result; +use mockall::automock; + +use crate::domain::CardOffersFetcher; +use crate::domain::ResponseMessage; +use crate::domain::ResponseMessageFormatter; +use crate::domain::{MessageSender, OriginalCardNameFetcher, UserQueryParser}; + +pub struct GetCardInfoHandler< + P: UserQueryParser, + N: OriginalCardNameFetcher, + S: MessageSender, + O: CardOffersFetcher, + F: ResponseMessageFormatter, +> { + parser: P, + name_fetcher: N, + sender: S, + offers_fetcher: O, + formatter: F, +} + +#[automock] +pub trait GetCardInfoUseCase { + fn get_card_info(&self, chat_id: i64, query: &str) -> impl Future>; +} + +impl< + P: UserQueryParser, + N: OriginalCardNameFetcher, + S: MessageSender, + O: CardOffersFetcher, + F: ResponseMessageFormatter, +> GetCardInfoHandler +{ + pub fn new(parser: P, name_fetcher: N, sender: S, offers_fetcher: O, formatter: F) -> Self { + Self { + parser, + name_fetcher, + sender, + offers_fetcher, + formatter, + } + } +} + +impl< + P: UserQueryParser, + N: OriginalCardNameFetcher, + S: MessageSender, + O: CardOffersFetcher, + F: ResponseMessageFormatter, +> GetCardInfoUseCase for GetCardInfoHandler +{ + async fn get_card_info(&self, chat_id: i64, query: &str) -> Result<()> { + let request = self.parser.parse_query(query); + let original_name = match self.name_fetcher.fetch_original_card_name(&request).await { + Ok(Some(name)) => name, + Ok(None) => { + let card_not_found = self + .formatter + .format_message(&ResponseMessage::CardNotFound); + self.sender + .send_message(chat_id, &card_not_found) + .await + .context("failed to send card not found message")?; + return Ok(()); + } + Err(err) => { + let service_unavailable = self + .formatter + .format_message(&ResponseMessage::ServiceUnavailable); + self.sender + .send_message(chat_id, &service_unavailable) + .await + .context("failed to send name fetcher error message")?; + return Err(err); + } + }; + let offers = match self.offers_fetcher.fetch_card_offers(&original_name).await { + Ok(offers) => offers, + Err(err) => { + let service_unavailable = self + .formatter + .format_message(&ResponseMessage::ServiceUnavailable); + self.sender + .send_message(chat_id, &service_unavailable) + .await + .context("failed to send offers fetcher error message")?; + return Err(err); + } + }; + let success = self.formatter.format_message(&ResponseMessage::Success { + original_name, + offers, + }); + self.sender + .send_message(chat_id, &success) + .await + .context("failed to send result message")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::domain::{ + CardInfoRequest, CardOffer, FormattedResponseMessage, MockCardOffersFetcher, + MockMessageSender, MockOriginalCardNameFetcher, MockResponseMessageFormatter, + MockUserQueryParser, OriginalCardName, + }; + + use super::*; + + #[tokio::test] + async fn test_get_card_name_error() { + let text = "Тефери, герой доминарии"; + + let mut parser = MockUserQueryParser::new(); + parser + .expect_parse_query() + .times(1) + .withf(move |query| query == text) + .returning(|_| CardInfoRequest::ByPrintedName { name: text.into() }); + + let mut name_fetcher = MockOriginalCardNameFetcher::new(); + name_fetcher + .expect_fetch_original_card_name() + .times(1) + .withf(|info| *info == CardInfoRequest::ByPrintedName { name: text.into() }) + .returning(|_| Box::pin(async { anyhow::bail!("error") })); + + let mut formatter = MockResponseMessageFormatter::new(); + formatter + .expect_format_message() + .times(1) + .withf(|message| matches!(message, ResponseMessage::ServiceUnavailable)) + .returning(|_| FormattedResponseMessage("service unavailable".into())); + + let mut sender = MockMessageSender::new(); + sender + .expect_send_message() + .times(1) + .withf(|chat_id, message| *chat_id == 123 && message.0 == "service unavailable") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let use_case = GetCardInfoHandler::new( + parser, + name_fetcher, + sender, + MockCardOffersFetcher::new(), + formatter, + ); + + use_case.get_card_info(123, text).await.unwrap_err(); + } + + #[tokio::test] + async fn test_get_card_does_not_exist() { + let text = "invalid"; + + let mut parser = MockUserQueryParser::new(); + parser + .expect_parse_query() + .times(1) + .withf(move |query| query == text) + .returning(|_| CardInfoRequest::ByPrintedName { name: text.into() }); + + let mut name_fetcher = MockOriginalCardNameFetcher::new(); + name_fetcher + .expect_fetch_original_card_name() + .times(1) + .withf(|info| *info == CardInfoRequest::ByPrintedName { name: text.into() }) + .returning(|_| Box::pin(async { Ok(None) })); + + let mut formatter = MockResponseMessageFormatter::new(); + formatter + .expect_format_message() + .times(1) + .withf(|message| matches!(message, ResponseMessage::CardNotFound)) + .returning(|_| FormattedResponseMessage("not found".into())); + + let mut sender = MockMessageSender::new(); + sender + .expect_send_message() + .times(1) + .withf(|chat_id, message| *chat_id == 123 && message.0 == "not found") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let use_case = GetCardInfoHandler::new( + parser, + name_fetcher, + sender, + MockCardOffersFetcher::new(), + formatter, + ); + + use_case.get_card_info(123, text).await.unwrap(); + } + + #[tokio::test] + async fn test_get_card_price_error() { + let text = "Тефери, герой доминарии"; + + let mut parser = MockUserQueryParser::new(); + parser + .expect_parse_query() + .times(1) + .withf(move |query| query == text) + .returning(|_| CardInfoRequest::ByPrintedName { name: text.into() }); + + let mut name_fetcher = MockOriginalCardNameFetcher::new(); + name_fetcher + .expect_fetch_original_card_name() + .times(1) + .withf(|info| *info == CardInfoRequest::ByPrintedName { name: text.into() }) + .returning(|_| { + Box::pin(async { Ok(Some(OriginalCardName("Teferi, Hero of Dominaria".into()))) }) + }); + + let mut offers_fetcher = MockCardOffersFetcher::new(); + offers_fetcher + .expect_fetch_card_offers() + .times(1) + .withf(|query| query.0 == "Teferi, Hero of Dominaria") + .returning(|_| Box::pin(async { anyhow::bail!("error") })); + + let mut formatter = MockResponseMessageFormatter::new(); + formatter + .expect_format_message() + .times(1) + .withf(|message| matches!(message, ResponseMessage::ServiceUnavailable)) + .returning(|_| FormattedResponseMessage("service unavailable".into())); + + let mut sender = MockMessageSender::new(); + sender + .expect_send_message() + .times(1) + .withf(|chat_id, message| *chat_id == 123 && message.0 == "service unavailable") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let use_case = + GetCardInfoHandler::new(parser, name_fetcher, sender, offers_fetcher, formatter); + + use_case.get_card_info(123, text).await.unwrap_err(); + } + + #[tokio::test] + async fn test_get_card_prices_success() { + let text = "Тефери, герой доминарии"; + + let mut parser = MockUserQueryParser::new(); + parser + .expect_parse_query() + .times(1) + .withf(move |query| query == text) + .returning(|_| CardInfoRequest::ByPrintedName { name: text.into() }); + + let mut name_fetcher = MockOriginalCardNameFetcher::new(); + name_fetcher + .expect_fetch_original_card_name() + .times(1) + .withf(|info| *info == CardInfoRequest::ByPrintedName { name: text.into() }) + .returning(|_| { + Box::pin(async { Ok(Some(OriginalCardName("Teferi, Hero of Dominaria".into()))) }) + }); + + let mut offers_fetcher = MockCardOffersFetcher::new(); + offers_fetcher + .expect_fetch_card_offers() + .times(1) + .withf(|query| query.0 == "Teferi, Hero of Dominaria") + .returning(|_| { + Box::pin(async { + Ok(vec![CardOffer { + price: "4.99".into(), + set: "Dominaria".into(), + link: "link".into(), + }]) + }) + }); + + let mut formatter = MockResponseMessageFormatter::new(); + formatter + .expect_format_message() + .times(1) + .withf(|message| match message { + ResponseMessage::Success { + original_name, + offers, + } => { + original_name.0 == "Teferi, Hero of Dominaria" + && *offers + == vec![CardOffer { + price: "4.99".into(), + set: "Dominaria".into(), + link: "link".into(), + }] + } + _ => false, + }) + .returning(|_| FormattedResponseMessage("success".into())); + + let mut sender = MockMessageSender::new(); + sender + .expect_send_message() + .times(1) + .withf(|chat_id, message| *chat_id == 123 && message.0 == "success") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let use_case = + GetCardInfoHandler::new(parser, name_fetcher, sender, offers_fetcher, formatter); + + use_case.get_card_info(123, text).await.unwrap(); + } +} diff --git a/src/domain.rs b/src/domain.rs new file mode 100644 index 0000000..2cf9dd0 --- /dev/null +++ b/src/domain.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use mockall::automock; + +#[automock] +pub trait UserQueryParser { + fn parse_query(&self, query: &str) -> CardInfoRequest; +} + +#[derive(PartialEq, Eq, Debug)] +pub enum CardInfoRequest { + ByPrintedName { name: String }, + BySetAndNumber { set: String, number: String }, +} + +#[automock] +pub trait OriginalCardNameFetcher { + fn fetch_original_card_name( + &self, + request: &CardInfoRequest, + ) -> impl Future>>; +} + +pub struct OriginalCardName(pub String); + +#[automock] +pub trait CardOffersFetcher { + fn fetch_card_offers( + &self, + original_name: &OriginalCardName, + ) -> impl Future>>; +} + +#[derive(Debug, PartialEq, Eq)] +pub struct CardOffer { + pub set: String, + pub price: String, + pub link: String, +} + +pub enum ResponseMessage { + Success { + original_name: OriginalCardName, + offers: Vec, + }, + CardNotFound, + ServiceUnavailable, +} + +#[automock] +pub trait ResponseMessageFormatter { + fn format_message(&self, message: &ResponseMessage) -> FormattedResponseMessage; +} + +pub struct FormattedResponseMessage(pub String); + +#[automock] +pub trait MessageSender { + fn send_message( + &self, + chat_id: i64, + message: &FormattedResponseMessage, + ) -> impl Future>; +} diff --git a/src/infrastructure.rs b/src/infrastructure.rs new file mode 100644 index 0000000..cca3f4e --- /dev/null +++ b/src/infrastructure.rs @@ -0,0 +1,5 @@ +pub mod scryfall; +pub mod starcitygames; +pub mod query; +pub mod vk; +pub mod telegram; diff --git a/src/infrastructure/query.rs b/src/infrastructure/query.rs new file mode 100644 index 0000000..a38fe5a --- /dev/null +++ b/src/infrastructure/query.rs @@ -0,0 +1,51 @@ +use crate::domain::{CardInfoRequest, UserQueryParser}; + +#[derive(Default)] +pub struct UserQueryChatParser; + +impl UserQueryChatParser { + pub fn new() -> Self { + Self + } +} + +impl UserQueryParser for UserQueryChatParser { + fn parse_query(&self, query: &str) -> crate::domain::CardInfoRequest { + let parts: Vec<&str> = query.split(" ").collect(); + match parts.as_slice() { + ["!s", set, code] => CardInfoRequest::BySetAndNumber { + set: (*set).into(), + number: (*code).into(), + }, + _ => CardInfoRequest::ByPrintedName { name: query.into() }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_name_request() { + let query = "Тефери, герой доминарии"; + let request: CardInfoRequest = UserQueryChatParser::new().parse_query(query); + assert_eq!( + request, + CardInfoRequest::ByPrintedName { name: query.into() } + ) + } + + #[test] + fn test_parse_set_and_code_request() { + let query = "!s GRN 1"; + let request: CardInfoRequest = UserQueryChatParser::new().parse_query(query); + assert_eq!( + request, + CardInfoRequest::BySetAndNumber { + set: "GRN".into(), + number: "1".into() + } + ) + } +} diff --git a/src/infrastructure/scryfall.rs b/src/infrastructure/scryfall.rs new file mode 100644 index 0000000..beb3b98 --- /dev/null +++ b/src/infrastructure/scryfall.rs @@ -0,0 +1,160 @@ +use anyhow::{Context, Result}; +use reqwest::{Client, StatusCode, Url}; +use serde::Deserialize; + +use crate::domain::{CardInfoRequest, OriginalCardName, OriginalCardNameFetcher}; + +pub struct ScryfallOriginalCardNameFetcher { + client: Client, + url: Url, +} + +#[derive(Deserialize)] +struct CardResponse { + name: String, +} + +impl ScryfallOriginalCardNameFetcher { + pub fn new(client: Client, url: Url) -> Self { + Self { client, url } + } +} + +impl OriginalCardNameFetcher for ScryfallOriginalCardNameFetcher { + async fn fetch_original_card_name( + &self, + request: &CardInfoRequest, + ) -> Result> { + let url = match request { + CardInfoRequest::ByPrintedName { name } => { + let mut url = self + .url + .join("/cards/named") + .context("failed to form scryfall named url")?; + url.query_pairs_mut().append_pair("fuzzy", name); + url + } + CardInfoRequest::BySetAndNumber { set, number } => self + .url + .join(&format!("/cards/{set}/{number}")) + .context("failed to form scryfall set+number url")?, + }; + let res = self + .client + .get(url) + .header("user-agent", "mtg-price-bot/1.0") + .header("accept", "application/json") + .send() + .await + .context("failed to get card info from scryfall")?; + if res.status() == StatusCode::NOT_FOUND { + return Ok(None); + } + let response = res + .json::() + .await + .context("failed to parse scryfall response")?; + Ok(Some(OriginalCardName(response.name))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_json::json; + #[tokio::test] + async fn test_search_card_by_printed_name() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("GET", "/cards/named") + .match_query(mockito::Matcher::UrlEncoded( + "fuzzy".into(), + "Тефери, герой доминарии".into(), + )) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({ + "name": "Teferi, Hero of Dominaria" + }) + .to_string(), + ) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + + let name = ScryfallOriginalCardNameFetcher::new(Client::new(), url) + .fetch_original_card_name(&CardInfoRequest::ByPrintedName { + name: "Тефери, герой доминарии".into(), + }) + .await + .unwrap(); + + assert_eq!(name.unwrap().0, "Teferi, Hero of Dominaria"); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_search_card_by_set_and_number() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("GET", "/cards/GRN/123") + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({ + "name": "Teferi, Hero of Dominaria" + }) + .to_string(), + ) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + + let name = ScryfallOriginalCardNameFetcher::new(Client::new(), url) + .fetch_original_card_name(&CardInfoRequest::BySetAndNumber { + set: "GRN".into(), + number: "123".into(), + }) + .await + .unwrap(); + + assert_eq!(name.unwrap().0, "Teferi, Hero of Dominaria"); + + mock.assert_async().await; + } + + #[tokio::test] + async fn test_search_card_not_found() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("GET", "/cards/named") + .match_query(mockito::Matcher::UrlEncoded( + "fuzzy".into(), + "Тефери, герой доминарии".into(), + )) + .with_status(404) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + + let result = ScryfallOriginalCardNameFetcher::new(Client::new(), url) + .fetch_original_card_name(&CardInfoRequest::ByPrintedName { + name: "Тефери, герой доминарии".into(), + }) + .await + .unwrap(); + + assert!(result.is_none()); + + mock.assert_async().await; + } +} diff --git a/src/infrastructure/starcitygames.rs b/src/infrastructure/starcitygames.rs new file mode 100644 index 0000000..a6e268a --- /dev/null +++ b/src/infrastructure/starcitygames.rs @@ -0,0 +1,209 @@ +use anyhow::{Context, Result}; +use reqwest::{Client, Url}; +use serde::Deserialize; +use serde_json::json; + +use crate::domain::{CardOffer, CardOffersFetcher, OriginalCardName}; + +pub struct StarCityGamesCardOffersFetcher { + client: Client, + search_url: Url, + display_url: String, + client_guid: String, + max_offers: usize, +} + +#[derive(Deserialize)] +struct Response { + #[serde(rename = "Results")] + results: Vec, +} + +#[derive(Deserialize)] +struct ResponseResult { + #[serde(rename = "Document")] + document: ResponseDocument, +} + +#[derive(Deserialize)] +struct ResponseDocument { + filter_set: Vec, + hawk_child_attributes: Vec, + url_detail: Vec, +} + +#[derive(Deserialize)] +pub struct ResponseHawkChildAttributes { + calculated_price: Vec, +} + +impl StarCityGamesCardOffersFetcher { + pub fn new( + client: Client, + search_url: Url, + display_url: String, + client_guid: String, + max_offers: usize, + ) -> Self { + Self { + client, + search_url, + display_url, + client_guid, + max_offers, + } + } +} + +impl CardOffersFetcher for StarCityGamesCardOffersFetcher { + async fn fetch_card_offers(&self, original_name: &OriginalCardName) -> Result> { + let url = self + .search_url + .join("/api/v2/search") + .context("failed to form starcity url")?; + let response = self + .client + .post(url) + .json(&json!( + { + "FacetSelections": { + "item_display_name": [ + original_name.0 + ] + }, + "clientguid": self.client_guid, + } + )) + .send() + .await + .context("failed to get cards from starcity api")?; + + let response: Response = response + .json() + .await + .context("failed to parse starcity response")?; + + let offers = response + .results + .into_iter() + .flat_map(|result| { + let mut document = result.document; + let set = document.filter_set.pop()?; + let price = document + .hawk_child_attributes + .pop()? + .calculated_price + .pop()?; + let link = format!("{}{}", self.display_url, document.url_detail.pop()?); + Some(CardOffer { set, price, link }) + }) + .take(self.max_offers) + .collect(); + + Ok(offers) + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case(1)] + #[case(2)] + #[tokio::test] + async fn test_search_offers(#[case] max_offers: usize) { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/api/v2/search") + .match_body(mockito::Matcher::Json(json!({ + "FacetSelections": { + "item_display_name": [ + "Teferi, Hero of Dominaria" + ] + }, + "clientguid": "guid", + }))) + .with_status(200) + .with_header("content-type", "application/json") + .with_body( + json!({ + "Results": [ + { + "Document": { + "filter_set": [ + "Dominaria (Foil)" + ], + "hawk_child_attributes": [ + { + "calculated_price": [ + "9.99" + ] + } + ], + "url_detail": [ + "/teferi-hero-of-dominaria-sgl-mtg-prm-prmr_2022_002-enn/" + ] + } + }, + { + "Document": { + "filter_set": [ + "Dominaria" + ], + "hawk_child_attributes": [ + { + "calculated_price": [ + "4.99" + ] + } + ], + "url_detail": [ + "/teferi-hero-of-dominaria-sgl-mtg-mb2-090-enn/" + ] + } + } + ] + }) + .to_string(), + ) + .create_async() + .await; + + let search_url = Url::parse(server.url().as_str()).unwrap(); + + let offers = StarCityGamesCardOffersFetcher::new( + Client::new(), + search_url, + "https://scg.com".into(), + "guid".into(), + max_offers, + ) + .fetch_card_offers(&OriginalCardName("Teferi, Hero of Dominaria".into())) + .await + .unwrap(); + let expect_offers: Vec = [ + CardOffer { + set: "Dominaria (Foil)".into(), + price: "9.99".into(), + link: "https://scg.com/teferi-hero-of-dominaria-sgl-mtg-prm-prmr_2022_002-enn/" + .into(), + }, + CardOffer { + set: "Dominaria".into(), + price: "4.99".into(), + link: "https://scg.com/teferi-hero-of-dominaria-sgl-mtg-mb2-090-enn/".into(), + }, + ] + .into_iter() + .take(max_offers) + .collect(); + + assert_eq!(offers, expect_offers); + + mock.assert_async().await; + } +} diff --git a/src/infrastructure/telegram.rs b/src/infrastructure/telegram.rs new file mode 100644 index 0000000..821a035 --- /dev/null +++ b/src/infrastructure/telegram.rs @@ -0,0 +1,178 @@ +use anyhow::{Context, Result}; +use reqwest::{Client, Url}; + +use crate::domain::{ + FormattedResponseMessage, MessageSender, ResponseMessage, ResponseMessageFormatter, +}; + +#[derive(Default)] +pub struct TelegramResponseFormatter; + +impl TelegramResponseFormatter { + pub fn new() -> Self { + Self + } +} + +impl ResponseMessageFormatter for TelegramResponseFormatter { + fn format_message(&self, info: &ResponseMessage) -> FormattedResponseMessage { + let message = match info { + ResponseMessage::Success { + original_name, + offers, + } => { + let offers: String = offers + .iter() + .enumerate() + .map(|(i, offer)| { + format!( + "{}. {}: ${}\n", + i + 1, + escape(&offer.link), + escape(&offer.set), + escape(&offer.price), + ) + }) + .collect(); + format!( + "Оригинальное название: {}\n\n{}", + escape(&original_name.0), + offers + ) + } + ResponseMessage::CardNotFound => "Карта не найдена".into(), + ResponseMessage::ServiceUnavailable => "Сервис временно недоступен".into(), + }; + FormattedResponseMessage(message) + } +} + +fn escape(text: &str) -> String { + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") +} + +pub struct TelegramMessageSender { + client: Client, + url: Url, + token: String, +} + +impl TelegramMessageSender { + pub fn new(client: Client, url: Url, token: String) -> Self { + Self { client, url, token } + } +} + +impl MessageSender for TelegramMessageSender { + async fn send_message(&self, chat_id: i64, message: &FormattedResponseMessage) -> Result<()> { + let url = self + .url + .join(&format!("/bot{}/sendMessage", self.token)) + .context("failed to form telegram url")?; + + self.client + .post(url) + .json(&serde_json::json!({ + "chat_id": chat_id, + "text": message.0, + "parse_mode": "HTML", + "link_preview_options": { + "is_disabled": true + } + })) + .send() + .await + .context("failed to send telegram message")?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use mockito::Matcher; + use reqwest::Client; + + use crate::domain::{CardOffer, OriginalCardName}; + + use super::*; + + #[test] + fn test_formatting_normal() { + let formatter = TelegramResponseFormatter::new(); + let original_name = OriginalCardName("Teferi, Hero of Dominaria".into()); + let offers = vec![ + CardOffer { + set: "Dominaria (Foil)".into(), + price: "9.99".into(), + link: "link1".into(), + }, + CardOffer { + set: "Dominaria".into(), + price: "4.99".into(), + link: "link2".into(), + }, + ]; + let result = formatter.format_message(&ResponseMessage::Success { + original_name, + offers, + }); + assert_eq!( + result.0, + "Оригинальное название: Teferi, Hero of Dominaria\n\n1. Dominaria (Foil): $9.99\n2. Dominaria: $4.99\n" + ); + } + + #[test] + fn test_formatting_escaping() { + let formatter = TelegramResponseFormatter::new(); + let original_name = OriginalCardName("<&>".into()); + let offers = vec![CardOffer { + set: "<&>".into(), + price: "<&>".into(), + link: "<&>".into(), + }]; + let result = formatter.format_message(&ResponseMessage::Success { + original_name, + offers, + }); + assert_eq!( + result.0, + "Оригинальное название: <&>\n\n1. <&>: $<&>\n" + ); + } + + #[tokio::test] + async fn test_message_send() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/bottoken/sendMessage") + .match_body(Matcher::Json(serde_json::json!( + { + "chat_id": 123, + "text": "not found", + "parse_mode": "HTML", + "link_preview_options": { + "is_disabled": true + } + } + ))) + .with_status(200) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + + let sender = TelegramMessageSender::new(Client::new(), url, "token".into()); + + sender + .send_message(123, &FormattedResponseMessage("not found".into())) + .await + .unwrap(); + + mock.assert_async().await + } +} diff --git a/src/infrastructure/vk.rs b/src/infrastructure/vk.rs new file mode 100644 index 0000000..1dc099e --- /dev/null +++ b/src/infrastructure/vk.rs @@ -0,0 +1,144 @@ +use crate::domain::{ + FormattedResponseMessage, MessageSender, ResponseMessage, ResponseMessageFormatter, +}; +use anyhow::{Context, Result}; +use reqwest::{Client, Url}; + +#[derive(Default)] +pub struct VkResponseFormatter; + +impl VkResponseFormatter { + pub fn new() -> Self { + Self + } +} + +impl ResponseMessageFormatter for VkResponseFormatter { + fn format_message(&self, message: &ResponseMessage) -> FormattedResponseMessage { + let result = match message { + ResponseMessage::Success { + original_name, + offers, + } => { + let offers: String = offers + .iter() + .enumerate() + .map(|(i, offer)| { + format!( + "{}. {}: ${}\n{}\n", + i + 1, + offer.set, + offer.price, + offer.link + ) + }) + .collect(); + format!("Оригинальное название: {}\n\n{}", original_name.0, offers) + } + ResponseMessage::CardNotFound => "Карта не найдена.".into(), + ResponseMessage::ServiceUnavailable => "Сервис временно недоступен.".into(), + }; + FormattedResponseMessage(result) + } +} + +pub struct VkMessageSender { + client: Client, + url: Url, + token: String, +} + +impl VkMessageSender { + pub fn new(client: Client, url: Url, token: String) -> Self { + Self { client, url, token } + } +} + +impl MessageSender for VkMessageSender { + async fn send_message(&self, chat_id: i64, message: &FormattedResponseMessage) -> Result<()> { + let mut url = self + .url + .join("/method/messages.send") + .context("failed to form vk url")?; + + url.query_pairs_mut() + .append_pair("user_id", chat_id.to_string().as_str()) + .append_pair("random_id", "0") + .append_pair("message", &message.0) + .append_pair("access_token", &self.token) + .append_pair("v", "5.199"); + + self.client + .post(url) + .send() + .await + .context("failed to send vk message")?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use mockito::Matcher; + + use crate::domain::{CardOffer, OriginalCardName}; + + use super::*; + + #[test] + fn test_format() { + let formatter = VkResponseFormatter::new(); + let original_name = OriginalCardName("Teferi, Hero of Dominaria".into()); + let offers = vec![ + CardOffer { + set: "Dominaria (Foil)".into(), + price: "9.99".into(), + link: "link1".into(), + }, + CardOffer { + set: "Dominaria".into(), + price: "4.99".into(), + link: "link2".into(), + }, + ]; + let result = formatter.format_message(&ResponseMessage::Success { + original_name, + offers, + }); + + assert_eq!( + result.0, + "Оригинальное название: Teferi, Hero of Dominaria\n\n1. Dominaria (Foil): $9.99\nlink1\n2. Dominaria: $4.99\nlink2\n" + ) + } + + #[tokio::test] + async fn test_send_message() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/method/messages.send") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded("user_id".into(), "123".parse().unwrap()), + Matcher::UrlEncoded("random_id".into(), "0".parse().unwrap()), + Matcher::UrlEncoded("message".into(), "not found".parse().unwrap()), + Matcher::UrlEncoded("access_token".into(), "token".parse().unwrap()), + Matcher::UrlEncoded("v".into(), "5.199".parse().unwrap()), + ])) + .with_status(200) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + + let sender = VkMessageSender::new(Client::new(), url, "token".into()); + + sender + .send_message(123, &FormattedResponseMessage("not found".into())) + .await + .unwrap(); + + mock.assert_async().await + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..c4e5009 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod domain; +pub mod application; +pub mod infrastructure; +pub mod presentation; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..80815c7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,191 @@ +use anyhow::Result; +use std::{env, sync::Arc}; + +use axum::{ + Router, + routing::{MethodRouter, post}, +}; +use mtg_price_bot::{ + application::GetCardInfoHandler, + infrastructure::{ + query::UserQueryChatParser, + scryfall::ScryfallOriginalCardNameFetcher, + starcitygames::StarCityGamesCardOffersFetcher, + telegram::{TelegramMessageSender, TelegramResponseFormatter}, + vk::{VkMessageSender, VkResponseFormatter}, + }, + presentation::{ + telegram::{TelegramState, TelegramWebhookSetter, handle_telegram_message}, + vk::{VkState, handle_vk_message}, + }, +}; +use reqwest::{Client, Url}; + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt().init(); + + let client = Client::new(); + let config = Config::from_env()?; + + let app = Router::new() + .route("/tg", get_telegram_handler(&config, client.clone()).await?) + .route("/vk", get_vk_handler(&config, client)); + + let listener = tokio::net::TcpListener::bind(config.bind_address).await?; + + axum::serve(listener, app).await?; + + Ok(()) +} + +async fn get_telegram_handler(config: &Config, client: Client) -> Result { + let setter = TelegramWebhookSetter::new( + client.clone(), + config.telegram.url.clone(), + config.telegram.secret.clone(), + config.telegram.token.clone(), + ); + + setter.set_webhook(&config.telegram.callback_url).await?; + + let parser = UserQueryChatParser::new(); + let name_fetcher = + ScryfallOriginalCardNameFetcher::new(client.clone(), config.scryfall_url.clone()); + let formatter = TelegramResponseFormatter::new(); + let telegram_sender = TelegramMessageSender::new( + client.clone(), + config.telegram.url.clone(), + config.telegram.token.clone(), + ); + let offers_fetcher = get_star_city_games_fetcher(&config.star_city_games, client.clone()); + + let handler = GetCardInfoHandler::new( + parser, + name_fetcher, + telegram_sender, + offers_fetcher, + formatter, + ); + + let state = TelegramState { + secret: config.telegram.secret.clone(), + handler, + }; + + Ok(post(handle_telegram_message).with_state(Arc::new(state))) +} + +fn get_vk_handler(config: &Config, client: Client) -> MethodRouter { + let parser = UserQueryChatParser::new(); + let name_fetcher = + ScryfallOriginalCardNameFetcher::new(client.clone(), config.scryfall_url.clone()); + let formatter = VkResponseFormatter::new(); + let sender = VkMessageSender::new( + client.clone(), + config.vk.url.clone(), + config.vk.token.clone(), + ); + let offers_fetcher = get_star_city_games_fetcher(&config.star_city_games, client.clone()); + + let handler = GetCardInfoHandler::new(parser, name_fetcher, sender, offers_fetcher, formatter); + + let state = VkState { + secret: config.vk.secret.clone(), + handler, + confirmation_code: config.vk.confirmation.clone(), + }; + + post(handle_vk_message).with_state(Arc::new(state)) +} + +fn get_star_city_games_fetcher( + config: &StarCityGamesConfig, + client: Client, +) -> StarCityGamesCardOffersFetcher { + StarCityGamesCardOffersFetcher::new( + client, + config.search_url.clone(), + config.display_url.clone(), + config.client_guid.clone(), + config.max_offers, + ) +} + +struct Config { + telegram: TelegramConfig, + vk: VkConfig, + star_city_games: StarCityGamesConfig, + scryfall_url: Url, + bind_address: String, +} + +impl Config { + fn from_env() -> Result { + let config = Self { + telegram: TelegramConfig::from_env()?, + vk: VkConfig::from_env()?, + star_city_games: StarCityGamesConfig::from_env()?, + scryfall_url: Url::parse(&env::var("SCRYFALL_URL")?)?, + bind_address: env::var("BIND_ADDRESS")?, + }; + Ok(config) + } +} + +struct TelegramConfig { + url: Url, + secret: String, + token: String, + callback_url: String, +} + +impl TelegramConfig { + fn from_env() -> Result { + let config = Self { + url: Url::parse(&env::var("TELEGRAM_API_URL")?)?, + secret: env::var("TELEGRAM_SECRET")?, + token: env::var("TELEGRAM_TOKEN")?, + callback_url: env::var("TELEGRAM_CALLBACK_URL")?, + }; + Ok(config) + } +} + +struct VkConfig { + url: Url, + token: String, + secret: String, + confirmation: String, +} + +impl VkConfig { + fn from_env() -> Result { + let config = Self { + url: Url::parse(&env::var("VK_API_URL")?)?, + token: env::var("VK_TOKEN")?, + secret: env::var("VK_SECRET")?, + confirmation: env::var("VK_CONFIRMATION_CODE")?, + }; + Ok(config) + } +} + +struct StarCityGamesConfig { + search_url: Url, + client_guid: String, + display_url: String, + max_offers: usize, +} + +impl StarCityGamesConfig { + fn from_env() -> Result { + let config = Self { + search_url: Url::parse(&env::var("SCG_SEARCH_URL")?)?, + client_guid: env::var("SCG_CLIENT_GUID")?, + display_url: env::var("SCG_DISPLAY_URL")?, + max_offers: env::var("SCG_MAX_OFFERS")?.parse()?, + }; + Ok(config) + } +} diff --git a/src/presentation.rs b/src/presentation.rs new file mode 100644 index 0000000..4375d70 --- /dev/null +++ b/src/presentation.rs @@ -0,0 +1,2 @@ +pub mod vk; +pub mod telegram; diff --git a/src/presentation/telegram.rs b/src/presentation/telegram.rs new file mode 100644 index 0000000..e49514a --- /dev/null +++ b/src/presentation/telegram.rs @@ -0,0 +1,182 @@ +use std::sync::Arc; + +use anyhow::Result; +use axum::{Json, extract::State, http::HeaderMap}; +use reqwest::{Client, StatusCode, Url}; +use serde::Deserialize; + +use crate::application::GetCardInfoUseCase; + +pub struct TelegramState { + pub secret: String, + pub handler: T, +} + +#[derive(Deserialize)] +pub struct TelegramUpdate { + message: Option, +} + +#[derive(Deserialize)] +struct TelegramMessage { + chat: TelegramChat, + text: Option, +} + +#[derive(Deserialize)] +struct TelegramChat { + id: i64, +} + +pub async fn handle_telegram_message( + headers: HeaderMap, + State(state): State>>, + Json(body): Json, +) -> StatusCode { + let secret_header = match headers.get("X-Telegram-Bot-Api-Secret-Token") { + Some(value) => value, + None => return StatusCode::FORBIDDEN, + }; + if secret_header.as_bytes() != state.secret.as_bytes() { + return StatusCode::FORBIDDEN; + } + let (chat_id, message) = match body { + TelegramUpdate { + message: + Some(TelegramMessage { + chat: TelegramChat { id }, + text: Some(text), + }), + } => (id, text), + _ => return StatusCode::OK, + }; + let res = state.handler.get_card_info(chat_id, &message).await; + if let Err(err) = res { + tracing::error!(?err); + } + StatusCode::OK +} + +pub struct TelegramWebhookSetter { + client: Client, + url: Url, + secret: String, + token: String, +} + +impl TelegramWebhookSetter { + pub fn new(client: Client, url: Url, secret: String, token: String) -> Self { + Self { + client, + url, + secret, + token, + } + } + + pub async fn set_webhook(&self, url: &str) -> Result<()> { + let mut api_url = self.url.join(&format!("/bot{}/setWebhook", self.token))?; + + api_url + .query_pairs_mut() + .append_pair("url", url) + .append_pair("secret_token", &self.secret); + + self.client.post(api_url).send().await?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use axum::http::HeaderMap; + use mockito::Matcher; + use reqwest::{Client, Url}; + + use crate::application::MockGetCardInfoUseCase; + + use super::*; + + #[tokio::test] + async fn test_handle_telegram_message_invalid_secret() { + let state = TelegramState { + secret: "secret".into(), + handler: MockGetCardInfoUseCase::new(), + }; + let mut headers = HeaderMap::new(); + headers.append( + "X-Telegram-Bot-Api-Secret-Token", + "invalid".parse().unwrap(), + ); + let code = handle_telegram_message( + headers, + State(Arc::new(state)), + Json(TelegramUpdate { message: None }), + ) + .await; + assert_eq!(code, StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn test_handle_telegram_message_update_without_message() { + let mut headers = HeaderMap::new(); + headers.append("X-Telegram-Bot-Api-Secret-Token", "secret".parse().unwrap()); + let state = TelegramState { + secret: "secret".into(), + handler: MockGetCardInfoUseCase::new(), + }; + let update = TelegramUpdate { message: None }; + let code = handle_telegram_message(headers, State(Arc::new(state)), Json(update)).await; + assert_eq!(code, StatusCode::OK); + } + + #[tokio::test] + async fn test_handle_telegram_message_success() { + let mut handler = MockGetCardInfoUseCase::new(); + handler + .expect_get_card_info() + .times(1) + .withf(|chat_id, message| *chat_id == 1 && message == "text") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let mut headers = HeaderMap::new(); + headers.append("X-Telegram-Bot-Api-Secret-Token", "secret".parse().unwrap()); + let state = TelegramState { + secret: "secret".into(), + handler, + }; + let update = TelegramUpdate { + message: Some(TelegramMessage { + chat: TelegramChat { id: 1 }, + text: Some("text".into()), + }), + }; + let code = handle_telegram_message(headers, State(Arc::new(state)), Json(update)).await; + assert_eq!(code, StatusCode::OK); + } + + #[tokio::test] + async fn test_set_telegram_webhook() { + let mut server = mockito::Server::new_async().await; + + let mock = server + .mock("POST", "/bottoken/setWebhook") + .match_query(Matcher::AllOf(vec![ + Matcher::UrlEncoded( + "url".into(), + "https://mtg-bot.flygrounder.ru/tg".parse().unwrap(), + ), + Matcher::UrlEncoded("secret_token".into(), "secret".parse().unwrap()), + ])) + .with_status(200) + .create_async() + .await; + + let url = Url::parse(server.url().as_str()).unwrap(); + let setter = TelegramWebhookSetter::new(Client::new(), url, "secret".into(), "token".into()); + setter.set_webhook("https://mtg-bot.flygrounder.ru/tg").await.unwrap(); + + mock.assert_async().await; + } +} diff --git a/src/presentation/vk.rs b/src/presentation/vk.rs new file mode 100644 index 0000000..53c052e --- /dev/null +++ b/src/presentation/vk.rs @@ -0,0 +1,162 @@ +use std::sync::Arc; + +use axum::{Json, extract::State}; +use reqwest::StatusCode; +use serde::Deserialize; + +use crate::application::GetCardInfoUseCase; +pub struct VkState { + pub secret: String, + pub handler: V, + pub confirmation_code: String, +} + +#[derive(Deserialize)] +#[serde(tag = "type")] +pub enum Request { + #[serde(rename = "message_new")] + MessageNew(MessageNew), + #[serde(rename = "confirmation")] + Confirmation(Confirmation), +} + +#[derive(Deserialize)] +pub struct Confirmation { + secret: String, +} + +#[derive(Deserialize)] +pub struct MessageNew { + object: Object, + secret: String, +} + +#[derive(Deserialize)] +struct Object { + message: Message, +} + +#[derive(Deserialize)] +struct Message { + from_id: i64, + text: String, +} + +pub async fn handle_vk_message( + State(state): State>>, + Json(request): Json, +) -> Result { + match request { + Request::MessageNew(request) => { + if request.secret != state.secret { + return Err(StatusCode::FORBIDDEN); + } + let message = request.object.message; + let result = state + .handler + .get_card_info(message.from_id, &message.text) + .await; + if let Err(err) = result { + tracing::error!(?err); + } + Ok("ok".into()) + } + Request::Confirmation(request) => { + if request.secret != state.secret { + return Err(StatusCode::FORBIDDEN); + } + Ok(state.confirmation_code.clone()) + }, + } +} + +#[cfg(test)] +mod tests { + use crate::application::MockGetCardInfoUseCase; + + use super::*; + + #[tokio::test] + async fn test_handle_vk_message_invalid_secret() { + let state = VkState { + secret: "secret".into(), + handler: MockGetCardInfoUseCase::new(), + confirmation_code: "confirmation".into(), + }; + let request = Request::MessageNew(MessageNew { + secret: "invalid".into(), + object: Object { + message: Message { + from_id: 1, + text: "text".into(), + }, + }, + }); + let code = handle_vk_message(State(Arc::new(state)), Json(request)) + .await + .unwrap_err(); + assert_eq!(code, StatusCode::FORBIDDEN); + } + + #[tokio::test] + async fn test_handle_vk_message_valid_secret() { + let mut handler = MockGetCardInfoUseCase::new(); + handler + .expect_get_card_info() + .times(1) + .withf(|chat_id, message| *chat_id == 1 && message == "text") + .returning(|_, _| Box::pin(async { Ok(()) })); + + let state = VkState { + secret: "secret".into(), + handler, + confirmation_code: "confirmation".into(), + }; + let request = Request::MessageNew(MessageNew { + secret: "secret".into(), + object: Object { + message: Message { + from_id: 1, + text: "text".into(), + }, + }, + }); + let result = handle_vk_message(State(Arc::new(state)), Json(request)) + .await + .unwrap(); + assert_eq!(result, "ok"); + } + + #[tokio::test] + async fn test_handle_confirmation_invalid_secret() { + let request = Request::Confirmation(Confirmation { + secret: "invalid".into(), + }); + let state = VkState { + secret: "secret".into(), + handler: MockGetCardInfoUseCase::new(), + confirmation_code: "confirmation".into(), + }; + let code = handle_vk_message(State(Arc::new(state)), Json(request)) + .await + .unwrap_err(); + assert_eq!(code, StatusCode::FORBIDDEN) + } + + #[tokio::test] + async fn test_handle_confirmation_success() { + let request = Request::Confirmation(Confirmation { + secret: "secret".into(), + }); + let state = VkState { + secret: "secret".into(), + handler: MockGetCardInfoUseCase::new(), + confirmation_code: "confirmation".into(), + }; + let confirmation = handle_vk_message(State(Arc::new(state)), Json(request)) + .await + .unwrap(); + + assert_eq!(confirmation, "confirmation") + } +}