diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 3fd3067..0000000 --- a/.dockerignore +++ /dev/null @@ -1,7 +0,0 @@ -.vscode - -build - -node_modules - -package-lock.json diff --git a/.github/workflows/build_docker_image.yml b/.github/workflows/build_docker_image.yml index b010bc5..629a7b1 100644 --- a/.github/workflows/build_docker_image.yml +++ b/.github/workflows/build_docker_image.yml @@ -12,7 +12,7 @@ jobs: - name: Checkout uses: actions/checkout@v3 - + - name: Set up QEMU uses: docker/setup-qemu-action@v2 @@ -23,6 +23,14 @@ jobs: name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - id: repository_name uses: ASzc/change-string-case-action@v2 with: @@ -30,7 +38,7 @@ jobs: - name: Login to ghcr.io - uses: docker/login-action@v2 + uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} @@ -48,8 +56,20 @@ jobs: tags: ghcr.io/${{ env.IMAGE }}:latest context: . file: ./docker/build.dockerfile + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,dest=/tmp/.buildx-cache-new - - + # This ugly bit is necessary if you don't want your cache to grow forever + # until it hits GitHub's limit of 5GB. + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache + + - name: Invoke deployment hook uses: joelwmale/webhook-action@master with: diff --git a/.gitignore b/.gitignore index 3fd3067..f5d27f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ .vscode -build +target -node_modules - -package-lock.json +test_env +.DS_Store diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..12397df --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2018 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7" + +[[package]] +name = "aquamarine" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a941c39708478e8eea39243b5983f1c42d2717b3620ee91f4a52115fd02ac43f" +dependencies = [ + "itertools", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "axum" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de18bc5f2e9df8f52da03856bf40e29b747de5a84e43aefff90e3dc4a21529b" +dependencies = [ + "async-trait", + "axum-core", + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4f44a0e6200e9d11a1cdc989e4b358f6e3d354fbf48478f345a17f4e43f8635" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", +] + +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "book_bot" +version = "0.1.0" +dependencies = [ + "base64", + "chrono", + "ctrlc", + "dateparser", + "env_logger", + "futures", + "lazy_static", + "log", + "regex", + "reqwest", + "sentry", + "serde", + "serde_json", + "strum", + "strum_macros", + "teloxide", + "textwrap", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "bumpalo" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" + +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time 0.1.44", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "ctrlc" +version = "3.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d91974fbbe88ec1df0c24a4f00f99583667a7e2e6272b2b92d294d81e462173" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "dateparser" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63256433b74eaddb73000f877bd9f4e6cea389bb7f64f349c6eb272044aa6a71" +dependencies = [ + "anyhow", + "chrono", + "lazy_static", + "regex", +] + +[[package]] +name = "debugid" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" +dependencies = [ + "serde", + "uuid", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "dptree" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81175dab5ec79c30e0576df2ed2c244e1721720c302000bb321b107e82e265c" +dependencies = [ + "futures", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "erasable" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f11890ce181d47a64e5d1eb4b6caba0e7bae911a356723740d058a5d0340b7d" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" + +[[package]] +name = "futures-task" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" + +[[package]] +name = "futures-util" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfe8eed0a9285ef776bb792479ea3834e8b94e13d615c2f66d03dd50a435a29" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "once_cell", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "itertools" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "js-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "matchit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73cbba799671b762df5a175adf59ce145165747bb891505c43d09aefbbf38beb" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "never" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91" + +[[package]] +name = "nix" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "openssl" +version = "0.10.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rc-box" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0690759eabf094030c2cdabc25ade1395bac02210d920d655053c1d49583fd8" +dependencies = [ + "erasable", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustversion" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "security-framework" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" + +[[package]] +name = "sentry" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73642819e7fa63eb264abc818a2f65ac8764afbe4870b5ee25bcecc491be0d4c" +dependencies = [ + "httpdate", + "reqwest", + "sentry-backtrace", + "sentry-contexts", + "sentry-core", + "sentry-panic", + "tokio", +] + +[[package]] +name = "sentry-backtrace" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49bafa55eefc6dbc04c7dac91e8c8ab9e89e9414f3193c105cabd991bbc75134" +dependencies = [ + "backtrace", + "once_cell", + "regex", + "sentry-core", +] + +[[package]] +name = "sentry-contexts" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c63317c4051889e73f0b00ce4024cae3e6a225f2e18a27d2c1522eb9ce2743da" +dependencies = [ + "hostname", + "libc", + "rustc_version", + "sentry-core", + "uname", +] + +[[package]] +name = "sentry-core" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a4591a2d128af73b1b819ab95f143bc6a2fbe48cd23a4c45e1ee32177e66ae6" +dependencies = [ + "once_cell", + "rand", + "sentry-types", + "serde", + "serde_json", +] + +[[package]] +name = "sentry-panic" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "696c74c5882d5a0d5b4a31d0ff3989b04da49be7983b7f52a52c667da5b480bf" +dependencies = [ + "sentry-backtrace", + "sentry-core", +] + +[[package]] +name = "sentry-types" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823923ae5f54a729159d720aa12181673044ee5c79cbda3be09e56f885e5468f" +dependencies = [ + "debugid", + "getrandom", + "hex", + "serde", + "serde_json", + "thiserror", + "time 0.3.14", + "url", + "uuid", +] + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smawk" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20518fe4a4c9acf048008599e464deb21beeae3d3578418951a189c235a7a9a8" + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "takecell" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20f34339676cdcab560c9a82300c4c2581f68b9369aedf0fae86f2ff9565ff3e" + +[[package]] +name = "teloxide" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91471f2d0f91b079b1b71dc689daac4748ded26a32c1cf0c6fc4130490c8f64e" +dependencies = [ + "aquamarine", + "axum", + "bytes", + "derive_more", + "dptree", + "futures", + "log", + "mime", + "pin-project", + "rand", + "serde", + "serde_json", + "serde_with_macros", + "teloxide-core", + "teloxide-macros", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tower", + "tower-http", + "url", +] + +[[package]] +name = "teloxide-core" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20608e89a267b68f3ac6e05228c89ebf455d6085388dd57250e3a00f1806e4e9" +dependencies = [ + "bitflags", + "bytes", + "chrono", + "derive_more", + "either", + "futures", + "log", + "mime", + "never", + "once_cell", + "pin-project", + "rc-box", + "reqwest", + "serde", + "serde_json", + "serde_with_macros", + "take_mut", + "takecell", + "thiserror", + "tokio", + "tokio-util", + "url", + "uuid", +] + +[[package]] +name = "teloxide-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d1f9fc42b44919d45dfcf8992d12e3df2a0d109431d2a15110d76fd3701a19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +dependencies = [ + "itoa", + "libc", + "num_threads", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c530c8675c1dbf98facee631536fa116b5fb6382d7dd6dc1b118d970eafe3ba" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-range-header", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "uname" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8" +dependencies = [ + "libc", +] + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" + +[[package]] +name = "unicode-linebreak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +dependencies = [ + "regex", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" + +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7e1ddeb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "book_bot" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1.20.1", features = ["rt-multi-thread", "macros"] } +reqwest = { version = "0.11.11", features = ["json"] } +serde = { version = "1.0.144", features = ["derive"] } +serde_json = "1.0.85" +log = "0.4" +env_logger = "0.9.0" +teloxide = { version = "0.10.1", features = ["macros", "auto-send", "webhooks-axum"] } +url = "2.2.2" +ctrlc = { version = "3.2.3", features = ["termination"] } +strum = "0.24" +strum_macros = "0.24" +futures = "0.3.24" +base64 = "0.13.0" +tokio-util = { version = "0.7.3", features = ["compat"] } +textwrap = "0.15.0" +regex = "1.6.0" +chrono = "0.4.22" +dateparser = "0.1.7" +sentry = "0.27.0" +lazy_static = "1.4.0" diff --git a/docker/build.dockerfile b/docker/build.dockerfile index fb8bc71..27440cf 100644 --- a/docker/build.dockerfile +++ b/docker/build.dockerfile @@ -1,22 +1,23 @@ -FROM node:lts-alpine as build-image +FROM rust:slim-bullseye as builder -WORKDIR /root/app +RUN apt-get update \ + && apt-get install -y pkg-config libssl-dev \ + && rm -rf /var/lib/apt/lists/* -COPY ./package.json ./ -COPY ./yarn.lock ./ -COPY ./tsconfig.json ./ -COPY ./src ./src +WORKDIR /usr/src/myapp +COPY . . -RUN yarn install --production && yarn build +RUN cargo install --path . -FROM node:lts-alpine as runtime-image +FROM debian:bullseye-slim -WORKDIR /root/app +RUN apt-get update \ + && apt-get install -y openssl ca-certificates \ + && rm -rf /var/lib/apt/lists/* -COPY ./package.json ./ -COPY ./scripts/healthcheck.js ./ +RUN update-ca-certificates -COPY --from=build-image /root/app/build ./build +COPY --from=builder /usr/local/cargo/bin/book_bot /usr/local/bin/book_bot -CMD yarn run run +CMD book_bot \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 8722dfb..0000000 --- a/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "flibusta_bot", - "version": "1.0.0", - "description": "", - "main": "src/index.ts", - "type": "module", - "scripts": { - "build": "esbuild ./src/main.ts --bundle --platform=node --outfile=./build/main.cjs", - "build-minify": "npm run build -- --minify --sourcemap", - "build-watch": "npm run build -- --watch", - "run": "node ./build/main.cjs", - "run-watch": "nodemon build/main.cjs" - }, - "author": "", - "license": "ISC", - "dependencies": { - "@sentry/node": "^7.8.1", - "chunk-text": "^2.0.1", - "debug": "^4.3.4", - "docker-ip-get": "^1.1.5", - "envalid": "^7.3.1", - "esbuild": "^0.14.53", - "express": "^4.18.1", - "got": "^12.3.0", - "js-base64": "^3.7.2", - "moment": "^2.29.4", - "redis": "^4.2.0", - "safe-compare": "^1.1.4", - "telegraf": "^4.8.6", - "typescript": "^4.7.4" - }, - "devDependencies": { - "@types/chunk-text": "^1.0.0", - "@types/debug": "^4.1.7", - "@types/express": "^4.17.13", - "@types/node": "^16.11.9", - "@types/safe-compare": "^1.1.0", - "nodemon": "^2.0.19" - } -} diff --git a/scripts/healthcheck.js b/scripts/healthcheck.js deleted file mode 100644 index b93d7f3..0000000 --- a/scripts/healthcheck.js +++ /dev/null @@ -1,20 +0,0 @@ -(async () => { - const http = await import('http'); - - const healthCheck = http.request("http://localhost:8080/healthcheck", (res) => { - console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); - if (res.statusCode == 200) { - process.exit(0); - } - else { - process.exit(1); - } - }); - - healthCheck.on('error', function (err) { - console.error('ERROR'); - process.exit(1); - }); - - healthCheck.end(); -})(); diff --git a/src/analytics/users_counter.ts b/src/analytics/users_counter.ts deleted file mode 100644 index a71767e..0000000 --- a/src/analytics/users_counter.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { createClient, RedisClientType } from 'redis'; - -import env from '@/config'; -import BotsManager from '@/bots/manager'; - -import Sentry from '@/sentry'; - - -enum RedisKeys { - UsersActivity = "users_activity", - UsersActivity24 = "users_activity_24", - RequestsCount = "requests_count", -} - - -export default class UsersCounter { - static _redisClient: RedisClientType | null = null; - - static async _getClient() { - if (this._redisClient === null) { - this._redisClient = createClient({ - url: `redis://${env.REDIS_HOST}:${env.REDIS_PORT}/${env.REDIS_DB}` - }); - - this._redisClient.on('error', (err) => { - console.log(err); - Sentry.captureException(err); - }); - - await this._redisClient.connect(); - } - - return this._redisClient; - } - - static async _getBotsUsernames(): Promise { - const promises = Object.values(BotsManager.bots).map(async (bot) => { - const botInfo = await bot.telegram.getMe(); - return botInfo.username; - }); - - return Promise.all(promises); - } - - static async _getUsersByBot(bot: string, lastDay: boolean): Promise { - const client = await this._getClient(); - - const prefix = lastDay ? RedisKeys.UsersActivity24 : RedisKeys.UsersActivity; - const template = `${prefix}_${bot}_`; - - return (await client.keys(template + '*')).map( - (key) => parseInt(key.replace(template, "")) - ); - } - - static async _getAllUsersCount(botsUsernames: string[], lastDay: boolean): Promise { - const users = new Set(); - - await Promise.all( - botsUsernames.map(async (bot) => { - (await this._getUsersByBot(bot, lastDay)).forEach((user) => users.add(user)); - }) - ); - - return users.size; - } - - static async _getUsersByBots(botsUsernames: string[], lastDay: boolean): Promise<{[bot: string]: number}> { - const result: {[bot: string]: number} = {}; - - await Promise.all( - botsUsernames.map(async (bot) => { - result[bot] = (await this._getUsersByBot(bot, lastDay)).length; - }) - ); - - return result; - } - - static async _incrementRequests(bot: string) { - const client = await this._getClient(); - - const key = `${RedisKeys.RequestsCount}_${bot}`; - - const exists = await client.exists(key); - - if (!exists) { - await client.set(key, 0); - } - - await client.incr(key); - } - - static async _getRequestsByBotCount(botsUsernames: string[]): Promise<{[bot: string]: number}> { - const client = await this._getClient(); - - const result: {[bot: string]: number} = {}; - - await Promise.all( - botsUsernames.map(async (bot) => { - const count = await client.get(`${RedisKeys.RequestsCount}_${bot}`); - result[bot] = count !== null ? parseInt(count) : 0; - }) - ); - - return result; - } - - static async take(userId: number, bot: string) { - const client = await this._getClient(); - - await client.set(`${RedisKeys.UsersActivity}_${bot}_${userId}`, 1); - await client.set(`${RedisKeys.UsersActivity24}_${bot}_${userId}`, 1, {EX: 24 * 60 * 60}); - - await this._incrementRequests(bot); - } - - static async getMetrics(): Promise { - const botUsernames = await this._getBotsUsernames(); - - const lines = []; - - lines.push(`all_users_count ${await this._getAllUsersCount(botUsernames, false)}`); - lines.push(`all_users_count_24h ${await this._getAllUsersCount(botUsernames, true)}`); - - const requestsByBotCount = await this._getRequestsByBotCount(botUsernames); - Object.keys(requestsByBotCount).forEach((bot: string) => { - lines.push(`requests_count{bot="${bot}"} ${requestsByBotCount[bot]}`); - }); - - const usersByBots = await this._getUsersByBots(botUsernames, false); - Object.keys(usersByBots).forEach((bot: string) => { - lines.push(`users_count{bot="${bot}"} ${usersByBots[bot]}`) - }); - - const usersByBots24h = await this._getUsersByBots(botUsernames, true); - Object.keys(usersByBots24h).forEach((bot: string) => { - lines.push(`users_count_24h{bot="${bot}"} ${usersByBots24h[bot]}`) - }); - - return lines.join("\n"); - } -} diff --git a/src/bots/approved_bot/mod.rs b/src/bots/approved_bot/mod.rs new file mode 100644 index 0000000..659756c --- /dev/null +++ b/src/bots/approved_bot/mod.rs @@ -0,0 +1,47 @@ +pub mod modules; +pub mod services; +mod tools; + +use teloxide::{prelude::*, types::BotCommand}; + +use self::modules::{ + annotations::get_annotations_handler, book::get_book_handler, download::get_download_hander, + help::get_help_handler, random::get_random_hander, search::get_search_hanlder, + settings::get_settings_handler, support::get_support_handler, + update_history::get_update_log_handler, +}; + +use super::{BotCommands, BotHandler}; + +pub fn get_approved_handler() -> (BotHandler, BotCommands) { + ( + dptree::entry() + .branch(get_help_handler()) + .branch(get_settings_handler()) + .branch(get_support_handler()) + .branch(get_random_hander()) + .branch(get_download_hander()) + .branch(get_annotations_handler()) + .branch(get_book_handler()) + .branch(get_update_log_handler()) + .branch(get_search_hanlder()), + Some(vec![ + BotCommand { + command: String::from("random"), + description: String::from("Попытать удачу"), + }, + BotCommand { + command: String::from("update_log"), + description: String::from("Обновления каталога"), + }, + BotCommand { + command: String::from("settings"), + description: String::from("Настройки"), + }, + BotCommand { + command: String::from("support"), + description: String::from("Поддержать разработчика"), + }, + ]), + ) +} diff --git a/src/bots/approved_bot/modules/annotations.rs b/src/bots/approved_bot/modules/annotations.rs new file mode 100644 index 0000000..68228a8 --- /dev/null +++ b/src/bots/approved_bot/modules/annotations.rs @@ -0,0 +1,354 @@ +use std::{convert::TryInto, str::FromStr}; + +use futures::TryStreamExt; +use regex::Regex; +use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*, types::*}; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +use crate::bots::{ + approved_bot::{ + modules::utils::generic_get_pagination_keyboard, + services::book_library::{ + get_author_annotation, get_book_annotation, + types::{AuthorAnnotation, BookAnnotation}, + }, + tools::filter_callback_query, + }, + BotHandlerInternal, +}; + +use super::utils::{filter_command, CommandParse, GetPaginationCallbackData}; + +#[derive(Clone)] +pub enum AnnotationCommand { + Book { id: u32 }, + Author { id: u32 }, +} + +impl CommandParse for AnnotationCommand { + fn parse(s: &str, bot_name: &str) -> Result { + let re = Regex::new(r"^/(?Pa|b)_an_(?P\d+)$").unwrap(); + + let full_bot_name = format!("@{bot_name}"); + let after_replace = s.replace(&full_bot_name, ""); + + let caps = re.captures(&after_replace); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let annotation_type = &caps["an_type"]; + let id: u32 = caps["id"].parse().unwrap(); + + match annotation_type { + "a" => Ok(AnnotationCommand::Author { id }), + "b" => Ok(AnnotationCommand::Book { id }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +#[derive(Clone)] +pub enum AnnotationCallbackData { + Book { id: u32, page: u32 }, + Author { id: u32, page: u32 }, +} + +impl FromStr for AnnotationCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + let re = Regex::new(r"^(?Pa|b)_an_(?P\d+)_(?P\d+)$").unwrap(); + + let caps = re.captures(s); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let annotation_type = &caps["an_type"]; + let id = caps["id"].parse::().unwrap(); + let page = caps["page"].parse::().unwrap(); + + match annotation_type { + "a" => Ok(AnnotationCallbackData::Author { id, page }), + "b" => Ok(AnnotationCallbackData::Book { id, page }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +impl ToString for AnnotationCallbackData { + fn to_string(&self) -> String { + match self { + AnnotationCallbackData::Book { id, page } => format!("b_an_{id}_{page}"), + AnnotationCallbackData::Author { id, page } => format!("a_an_{id}_{page}"), + } + } +} + +pub trait AnnotationFormat { + fn get_file(&self) -> Option<&String>; + fn get_text(&self) -> &str; + + fn is_normal_text(&self) -> bool; +} + +impl AnnotationFormat for BookAnnotation { + fn get_file(&self) -> Option<&String> { + self.file.as_ref() + } + + fn get_text(&self) -> &str { + self.text.as_str() + } + + fn is_normal_text(&self) -> bool { + self.text.replace('\n', "").replace(' ', "").len() != 0 + } +} + +impl GetPaginationCallbackData for AnnotationCallbackData { + fn get_pagination_callback_data(&self, target_page: u32) -> String { + match self { + AnnotationCallbackData::Book { id, .. } => AnnotationCallbackData::Book { + id: id.clone(), + page: target_page, + }, + AnnotationCallbackData::Author { id, .. } => AnnotationCallbackData::Author { + id: id.clone(), + page: target_page, + }, + } + .to_string() + } +} + +impl AnnotationFormat for AuthorAnnotation { + fn get_file(&self) -> Option<&String> { + self.file.as_ref() + } + + fn get_text(&self) -> &str { + self.text.as_str() + } + + fn is_normal_text(&self) -> bool { + self.text.replace('\n', "").replace(' ', "").len() != 0 + } +} + +async fn download_image( + file: &String, +) -> Result> { + let response = reqwest::get(file).await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + Ok(response) +} + +pub async fn send_annotation_handler( + message: Message, + bot: AutoSend, + command: AnnotationCommand, + annotation_getter: fn(id: u32) -> Fut, +) -> BotHandlerInternal +where + T: AnnotationFormat, + Fut: std::future::Future>>, +{ + let id = match command { + AnnotationCommand::Book { id } => id, + AnnotationCommand::Author { id } => id, + }; + + let annotation = match annotation_getter(id).await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + if annotation.get_file().is_none() && !annotation.is_normal_text() { + return match bot + .send_message(message.chat.id, "Аннотация недоступна :(") + .reply_to_message_id(message.id) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + }; + }; + + if let Some(file) = annotation.get_file() { + let image_response = download_image(file).await; + + if let Ok(v) = image_response { + let data = v + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read() + .compat(); + + log::info!("{}", file); + + match bot + .send_photo(message.chat.id, InputFile::read(data)) + .send() + .await + { + Ok(_) => (), + Err(err) => log::info!("{}", err), + } + } + }; + + if !annotation.is_normal_text() { + return Ok(()); + } + + let chunked_text: Vec = textwrap::wrap(annotation.get_text(), 512) + .into_iter() + .filter(|text| text.replace('\r', "").len() != 0) + .map(|text| text.to_string()) + .collect(); + let current_text = chunked_text.get(0).unwrap(); + + let callback_data = match command { + AnnotationCommand::Book { id } => AnnotationCallbackData::Book { id, page: 1 }, + AnnotationCommand::Author { id } => AnnotationCallbackData::Author { id, page: 1 }, + }; + let keyboard = generic_get_pagination_keyboard( + 1, + chunked_text.len().try_into().unwrap(), + callback_data, + false, + ); + + match bot + .send_message(message.chat.id, current_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn annotation_pagination_handler( + cq: CallbackQuery, + bot: AutoSend, + callback_data: AnnotationCallbackData, + annotation_getter: fn(id: u32) -> Fut, +) -> BotHandlerInternal +where + T: AnnotationFormat, + Fut: std::future::Future>>, +{ + let (id, page) = match callback_data { + AnnotationCallbackData::Book { id, page } => (id, page), + AnnotationCallbackData::Author { id, page } => (id, page), + }; + + let annotation = match annotation_getter(id).await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + let message = match cq.message { + Some(v) => v, + None => return Ok(()), + }; + + let page_index: usize = page.try_into().unwrap(); + let chunked_text: Vec = textwrap::wrap(annotation.get_text(), 512) + .into_iter() + .filter(|text| text.replace('\r', "").len() != 0) + .map(|text| text.to_string()) + .collect(); + let current_text = chunked_text.get(page_index - 1).unwrap(); + + let keyboard = generic_get_pagination_keyboard( + page, + chunked_text.len().try_into().unwrap(), + callback_data, + false, + ); + + match bot + .edit_message_text(message.chat.id, message.id, current_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_annotations_handler() -> crate::bots::BotHandler { + dptree::entry() + .branch( + Update::filter_message() + .chain(filter_command::()) + .endpoint( + |message: Message, bot: AutoSend, command: AnnotationCommand| async move { + match command { + AnnotationCommand::Book { .. } => { + send_annotation_handler(message, bot, command, get_book_annotation) + .await + } + AnnotationCommand::Author { .. } => { + send_annotation_handler( + message, + bot, + command, + get_author_annotation, + ) + .await + } + } + }, + ), + ) + .branch( + Update::filter_callback_query() + .chain(filter_callback_query::()) + .endpoint( + |cq: CallbackQuery, + bot: AutoSend, + callback_data: AnnotationCallbackData| async move { + match callback_data { + AnnotationCallbackData::Book { .. } => { + annotation_pagination_handler( + cq, + bot, + callback_data, + get_book_annotation, + ) + .await + } + AnnotationCallbackData::Author { .. } => { + annotation_pagination_handler( + cq, + bot, + callback_data, + get_author_annotation, + ) + .await + } + } + }, + ), + ) +} diff --git a/src/bots/approved_bot/modules/book.rs b/src/bots/approved_bot/modules/book.rs new file mode 100644 index 0000000..6c71ecb --- /dev/null +++ b/src/bots/approved_bot/modules/book.rs @@ -0,0 +1,346 @@ +use std::str::FromStr; + +use regex::Regex; +use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*}; + +use crate::bots::approved_bot::{ + services::{ + book_library::{ + formaters::Format, get_author_books, get_sequence_books, get_translator_books, + types::Page, + }, + user_settings::get_user_or_default_lang_codes, + }, + tools::filter_callback_query, +}; + +use super::utils::{ + filter_command, generic_get_pagination_keyboard, CommandParse, GetPaginationCallbackData, +}; + +#[derive(Clone)] +pub enum BookCommand { + Author { id: u32 }, + Translator { id: u32 }, + Sequence { id: u32 }, +} + +impl CommandParse for BookCommand { + fn parse(s: &str, bot_name: &str) -> Result { + let re = Regex::new(r"^/(?Pa|t|s)_(?P\d+)$").unwrap(); + + let full_bot_name = format!("@{bot_name}"); + let after_replace = s.replace(&full_bot_name, ""); + + let caps = re.captures(&after_replace); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let annotation_type = &caps["an_type"]; + let id: u32 = caps["id"].parse().unwrap(); + + match annotation_type { + "a" => Ok(BookCommand::Author { id }), + "t" => Ok(BookCommand::Translator { id }), + "s" => Ok(BookCommand::Sequence { id }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +#[derive(Clone)] +pub enum BookCallbackData { + Author { id: u32, page: u32 }, + Translator { id: u32, page: u32 }, + Sequence { id: u32, page: u32 }, +} + +impl FromStr for BookCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + let re = Regex::new(r"^b(?Pa|t|s)_(?P\d+)_(?P\d+)$").unwrap(); + + let caps = re.captures(s); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let annotation_type = &caps["an_type"]; + let id = caps["id"].parse::().unwrap(); + let page = caps["page"].parse::().unwrap(); + + match annotation_type { + "a" => Ok(BookCallbackData::Author { id, page }), + "t" => Ok(BookCallbackData::Translator { id, page }), + "s" => Ok(BookCallbackData::Sequence { id, page }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +impl ToString for BookCallbackData { + fn to_string(&self) -> String { + match self { + BookCallbackData::Author { id, page } => format!("ba_{id}_{page}"), + BookCallbackData::Translator { id, page } => format!("bt_{id}_{page}"), + BookCallbackData::Sequence { id, page } => format!("bs_{id}_{page}"), + } + } +} + +impl GetPaginationCallbackData for BookCallbackData { + fn get_pagination_callback_data(&self, target_page: u32) -> String { + match self { + BookCallbackData::Author { id, .. } => BookCallbackData::Author { + id: id.clone(), + page: target_page, + }, + BookCallbackData::Translator { id, .. } => BookCallbackData::Translator { + id: id.clone(), + page: target_page, + }, + BookCallbackData::Sequence { id, .. } => BookCallbackData::Sequence { + id: id.clone(), + page: target_page, + }, + } + .to_string() + } +} + +async fn send_book_handler( + message: Message, + bot: AutoSend, + command: BookCommand, + books_getter: fn(id: u32, page: u32, allowed_langs: Vec) -> Fut, +) -> crate::bots::BotHandlerInternal +where + T: Format + Clone, + Fut: std::future::Future, Box>>, +{ + let id = match command { + BookCommand::Author { id } => id, + BookCommand::Translator { id } => id, + BookCommand::Sequence { id } => id, + }; + + let chat_id = message.chat.id; + let user_id = message.from().map(|from| from.id); + + let user_id = match user_id { + Some(v) => v, + None => { + return match bot + .send_message(chat_id, "Повторите запрос сначала") + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + }; + + let allowed_langs = get_user_or_default_lang_codes(user_id).await; + + let items_page = match books_getter(id, 1, allowed_langs.clone()).await { + Ok(v) => v, + Err(err) => { + match bot + .send_message(chat_id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + return Err(err); + } + }; + + if items_page.total_pages == 0 { + match bot.send_message(chat_id, "Книги не найдены!").send().await { + Ok(_) => (), + Err(err) => return Err(Box::new(err)), + }; + }; + + let formated_items = items_page.format_items(); + let total_pages = items_page.total_pages; + + let footer = format!("\n\nСтраница 1/{total_pages}"); + let message_text = format!("{formated_items}{footer}"); + + let callback_data = match command { + BookCommand::Author { id } => BookCallbackData::Author { id, page: 1 }, + BookCommand::Translator { id } => BookCallbackData::Translator { id, page: 1 }, + BookCommand::Sequence { id } => BookCallbackData::Sequence { id, page: 1 }, + }; + + let keyboard = generic_get_pagination_keyboard(1, total_pages, callback_data, true); + + match bot + .send_message(chat_id, message_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +async fn send_pagination_book_handler( + cq: CallbackQuery, + bot: AutoSend, + callback_data: BookCallbackData, + books_getter: fn(id: u32, page: u32, allowed_langs: Vec) -> Fut, +) -> crate::bots::BotHandlerInternal +where + T: Format + Clone, + Fut: std::future::Future, Box>>, +{ + let (id, page) = match callback_data { + BookCallbackData::Author { id, page } => (id, page), + BookCallbackData::Translator { id, page } => (id, page), + BookCallbackData::Sequence { id, page } => (id, page), + }; + + let chat_id = cq.message.as_ref().map(|message| message.chat.id); + let user_id = cq + .message + .as_ref() + .map(|message| message.from().map(|from| from.id)) + .unwrap_or(None); + let message_id = cq.message.as_ref().map(|message| message.id); + + let (chat_id, user_id, message_id) = match (chat_id, user_id, message_id) { + (Some(chat_id), Some(user_id), Some(message_id)) => (chat_id, user_id, message_id), + _ => { + return match chat_id { + Some(v) => match bot.send_message(v, "Повторите поиск сначала").send().await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + }, + None => return Ok(()), + } + } + }; + + let allowed_langs = get_user_or_default_lang_codes(user_id).await; + + let mut items_page = match books_getter(id, page, allowed_langs.clone()).await { + Ok(v) => v, + Err(err) => { + match bot + .send_message(chat_id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + return Err(err); + } + }; + + if items_page.total_pages == 0 { + match bot.send_message(chat_id, "Книги не найдены!").send().await { + Ok(_) => (), + Err(err) => return Err(Box::new(err)), + }; + }; + + if page > items_page.total_pages { + items_page = match books_getter(id, items_page.total_pages, allowed_langs.clone()).await { + Ok(v) => v, + Err(err) => { + match bot + .send_message(chat_id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + return Err(err); + } + }; + } + + let formated_items = items_page.format_items(); + + let total_pages = items_page.total_pages; + + let footer = format!("\n\nСтраница {page}/{total_pages}"); + let message_text = format!("{formated_items}{footer}"); + + let keyboard = generic_get_pagination_keyboard(page, total_pages, callback_data, true); + + match bot + .edit_message_text(chat_id, message_id, message_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_book_handler() -> crate::bots::BotHandler { + dptree::entry() + .branch( + Update::filter_message() + .chain(filter_command::()) + .endpoint( + |message: Message, bot: AutoSend, command: BookCommand| async move { + match command { + BookCommand::Author { .. } => { + send_book_handler( + message, + bot, + command, + get_author_books, + ) + .await + } + BookCommand::Translator { .. } => { + send_book_handler( + message, + bot, + command, + get_translator_books, + ) + .await + } + BookCommand::Sequence { .. } => { + send_book_handler( + message, + bot, + command, + get_sequence_books, + ) + .await + } + } + }, + ), + ) + .branch( + Update::filter_callback_query() + .chain(filter_callback_query::()) + .endpoint(|cq: CallbackQuery, bot: AutoSend, callback_data: BookCallbackData| async move { + match callback_data { + BookCallbackData::Author { .. } => send_pagination_book_handler(cq, bot, callback_data, get_author_books).await, + BookCallbackData::Translator { .. } => send_pagination_book_handler(cq, bot, callback_data, get_translator_books).await, + BookCallbackData::Sequence { .. } => send_pagination_book_handler(cq, bot, callback_data, get_sequence_books).await, + } + }), + ) +} diff --git a/src/bots/approved_bot/modules/download.rs b/src/bots/approved_bot/modules/download.rs new file mode 100644 index 0000000..7dbca28 --- /dev/null +++ b/src/bots/approved_bot/modules/download.rs @@ -0,0 +1,153 @@ +use futures::TryStreamExt; +use regex::Regex; +use teloxide::{dispatching::UpdateFilterExt, dptree, prelude::*, types::*}; +use tokio_util::compat::FuturesAsyncReadCompatExt; + +use crate::{ + bots::{ + approved_bot::services::book_cache::{ + clear_book_cache, download_file, get_cached_message, + types::{CachedMessage, DownloadFile}, + }, + BotHandlerInternal, + }, + bots_manager::BotCache, +}; + +use super::utils::{filter_command, CommandParse}; + +#[derive(Clone)] +pub struct DownloadData { + pub format: String, + pub id: u32, +} + +impl CommandParse for DownloadData { + fn parse(s: &str, bot_name: &str) -> Result { + let re = Regex::new(r"^/d_(?P[a-zA-Z0-9]+)_(?P\d+)$").unwrap(); + + let full_bot_name = format!("@{bot_name}"); + let after_replace = s.replace(&full_bot_name, ""); + + let caps = re.captures(&after_replace); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let file_format = &caps["file_format"]; + let book_id: u32 = caps["book_id"].parse().unwrap(); + + Ok(DownloadData { + format: file_format.to_string(), + id: book_id, + }) + } +} + +async fn _send_cached( + message: Message, + bot: AutoSend, + cached_message: CachedMessage, +) -> BotHandlerInternal { + match bot + .copy_message( + message.chat.id, + Recipient::Id(ChatId(cached_message.chat_id)), + cached_message.message_id, + ) + .send() + .await + { + Ok(_) => todo!(), + Err(err) => Err(Box::new(err)), + } +} + +async fn send_cached_message( + message: Message, + bot: AutoSend, + download_data: DownloadData, +) -> BotHandlerInternal { + let cached_message = get_cached_message(&download_data).await; + match cached_message { + Ok(v) => match _send_cached(message.clone(), bot.clone(), v).await { + Ok(_) => return Ok(()), + Err(err) => log::info!("{:?}", err), + }, + Err(err) => return Err(err), + }; + + match clear_book_cache(&download_data).await { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + }; + + let cached_message = get_cached_message(&download_data).await; + match cached_message { + Ok(v) => _send_cached(message, bot, v).await, + Err(err) => return Err(err), + } +} + +async fn send_with_download_from_channel( + message: Message, + bot: AutoSend, + download_data: DownloadData, +) -> BotHandlerInternal { + let downloaded_file = match download_file(&download_data).await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + let DownloadFile { + response, + filename, + caption, + } = downloaded_file; + + let data = response + .bytes_stream() + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .into_async_read() + .compat(); + + let document: InputFile = InputFile::read(data).file_name(filename); + + match bot + .send_document(message.chat.id, document) + .caption(caption) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +async fn download_handler( + message: Message, + bot: AutoSend, + cache: BotCache, + download_data: DownloadData, +) -> BotHandlerInternal { + match cache { + BotCache::Original => send_cached_message(message, bot, download_data).await, + BotCache::NoCache => send_with_download_from_channel(message, bot, download_data).await, + } +} + +pub fn get_download_hander() -> crate::bots::BotHandler { + dptree::entry().branch( + Update::filter_message() + .chain(filter_command::()) + .endpoint( + |message: Message, + bot: AutoSend, + cache: BotCache, + download_data: DownloadData| async move { + download_handler(message, bot, cache, download_data).await + }, + ), + ) +} diff --git a/src/bots/approved_bot/modules/help.rs b/src/bots/approved_bot/modules/help.rs new file mode 100644 index 0000000..4222f55 --- /dev/null +++ b/src/bots/approved_bot/modules/help.rs @@ -0,0 +1,45 @@ +use crate::bots::BotHandlerInternal; + +use teloxide::{prelude::*, utils::command::BotCommands}; + +#[derive(BotCommands, Clone)] +#[command(rename = "lowercase")] +enum HelpCommand { + Start, + Help, +} + +pub async fn help_handler(message: Message, bot: AutoSend) -> BotHandlerInternal { + let name = message + .from() + .map(|user| user.first_name.clone()) + .unwrap_or("пользователь".to_string()); + + match bot + .send_message( + message.chat.id, + format!( + " +Привет, {name}! \n +Этот бот поможет тебе загружать книги.\n +Настройки языков для поиска /settings.\n + " + ), + ) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_help_handler() -> crate::bots::BotHandler { + dptree::entry().branch( + Update::filter_message().branch( + dptree::entry() + .filter_command::() + .endpoint(|message, bot| async move { help_handler(message, bot).await }), + ), + ) +} diff --git a/src/bots/approved_bot/modules/mod.rs b/src/bots/approved_bot/modules/mod.rs new file mode 100644 index 0000000..5bc93f9 --- /dev/null +++ b/src/bots/approved_bot/modules/mod.rs @@ -0,0 +1,10 @@ +pub mod annotations; +pub mod book; +pub mod download; +pub mod help; +pub mod random; +pub mod search; +pub mod settings; +pub mod support; +pub mod update_history; +pub mod utils; diff --git a/src/bots/approved_bot/modules/random.rs b/src/bots/approved_bot/modules/random.rs new file mode 100644 index 0000000..9bb2702 --- /dev/null +++ b/src/bots/approved_bot/modules/random.rs @@ -0,0 +1,364 @@ +use strum_macros::{Display, EnumIter}; +use teloxide::{ + prelude::*, + types::{InlineKeyboardButton, InlineKeyboardMarkup}, + utils::command::BotCommands, +}; + +use crate::bots::{ + approved_bot::{ + services::{ + book_library::{self, formaters::Format}, + user_settings::get_user_or_default_lang_codes, + }, + tools::filter_callback_query, + }, + BotHandlerInternal, +}; + +#[derive(BotCommands, Clone)] +#[command(rename = "lowercase")] +enum RandomCommand { + Random, +} + +#[derive(Clone, Display, EnumIter)] +#[strum(serialize_all = "snake_case")] +enum RandomCallbackData { + RandomBook, + RandomAuthor, + RandomSequence, + RandomBookByGenreRequest, + Genres { index: u32 }, + RandomBookByGenre { id: u32 }, +} + +impl std::str::FromStr for RandomCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + let value = s.to_string(); + + for callback_data in ::iter() { + match callback_data { + RandomCallbackData::Genres { .. } + | RandomCallbackData::RandomBookByGenre { .. } => { + let callback_prefix = callback_data.to_string(); + + if value.starts_with(&callback_prefix) { + let data: u32 = value + .strip_prefix(&format!("{}_", &callback_prefix).to_string()) + .unwrap() + .parse() + .unwrap(); + + match callback_data { + RandomCallbackData::Genres { .. } => { + return Ok(RandomCallbackData::Genres { index: data }) + } + RandomCallbackData::RandomBookByGenre { .. } => { + return Ok(RandomCallbackData::RandomBookByGenre { id: data }) + } + _ => (), + } + } + } + _ => { + if value == callback_data.to_string() { + return Ok(callback_data); + } + } + } + } + + return Err(strum::ParseError::VariantNotFound); + } +} + +async fn random_handler(message: Message, bot: AutoSend) -> crate::bots::BotHandlerInternal { + const MESSAGE_TEXT: &str = "Что хотим получить?"; + + let keyboard = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + RandomCallbackData::RandomBook.to_string(), + ), + text: String::from("Книгу"), + }], + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + RandomCallbackData::RandomBookByGenreRequest.to_string(), + ), + text: String::from("Книгу по жанру"), + }], + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + RandomCallbackData::RandomAuthor.to_string(), + ), + text: String::from("Автора"), + }], + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + RandomCallbackData::RandomSequence.to_string(), + ), + text: String::from("Серию"), + }], + ], + }; + + let res = bot + .send_message(message.chat.id, MESSAGE_TEXT) + .reply_to_message_id(message.id) + .reply_markup(keyboard) + .send() + .await; + + match res { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +async fn get_random_item_handler_internal( + cq: CallbackQuery, + bot: AutoSend, + item: Result>, +) -> BotHandlerInternal +where + T: Format, +{ + match item { + Ok(item) => { + let item_message = item.format(); + + let send_item_handler = tokio::spawn( + bot.send_message(cq.from.id, item_message) + .reply_markup(InlineKeyboardMarkup { + inline_keyboard: vec![vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + cq.data.unwrap(), + ), + text: String::from("Повторить?"), + }]], + }) + .send(), + ); + + cq.message.map(|message| async move { + bot.edit_message_reply_markup(message.chat.id, message.id) + .reply_markup(InlineKeyboardMarkup { + inline_keyboard: vec![], + }) + .send() + .await + }); + + match send_item_handler.await { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + Err(err) => { + match bot + .send_message(cq.from.id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(int_error) => return Err(Box::new(int_error)), + } + + return Err(err); + } + } +} + +async fn get_random_item_handler( + cq: CallbackQuery, + bot: AutoSend, + item_getter: fn(allowed_langs: Vec) -> Fut, +) -> BotHandlerInternal +where + T: Format, + Fut: std::future::Future>>, +{ + let allowed_langs = get_user_or_default_lang_codes(cq.from.id).await; + + let item = item_getter(allowed_langs).await; + + get_random_item_handler_internal(cq, bot, item).await +} + +async fn get_genre_metas_handler(cq: CallbackQuery, bot: AutoSend) -> BotHandlerInternal { + let genre_metas = match book_library::get_genre_metas().await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + match cq.message { + Some(message) => { + let keyboard = InlineKeyboardMarkup { + inline_keyboard: genre_metas + .clone() + .into_iter() + .enumerate() + .map(|(index, genre_meta)| { + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(format!( + "{}_{index}", + RandomCallbackData::Genres { + index: index as u32 + } + .to_string() + )), + text: genre_meta, + }] + }) + .collect(), + }; + + match bot + .edit_message_reply_markup(message.chat.id, message.id) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + None => { + match bot + .send_message(cq.from.id, "Ошибка! Начните заново :(") + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + } +} + +async fn get_genres_by_meta_handler( + cq: CallbackQuery, + bot: AutoSend, + genre_index: u32, +) -> BotHandlerInternal { + let genre_metas = match book_library::get_genre_metas().await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + let meta = match genre_metas.get(genre_index as usize) { + Some(v) => v, + None => { + return match bot + .send_message(cq.from.id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + }; + + let genres = match book_library::get_genres(meta.to_string()).await { + Ok(v) => v.items, + Err(err) => return Err(err), + }; + + let mut buttons: Vec> = genres + .clone() + .into_iter() + .map(|genre| { + vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(format!( + "{}_{}", + RandomCallbackData::RandomBookByGenre { id: genre.id }.to_string(), + genre.id + )), + text: genre.description, + }] + }) + .collect(); + + buttons.push(vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + RandomCallbackData::RandomBookByGenreRequest.to_string(), + ), + text: "< Назад >".to_string(), + }]); + + let keyboard = InlineKeyboardMarkup { + inline_keyboard: buttons, + }; + + match cq.message { + Some(message) => { + match bot + .edit_message_reply_markup(message.chat.id, message.id) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + None => { + match bot + .send_message(cq.from.id, "Ошибка! Начните заново :(") + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } + } + } +} + +async fn get_random_book_by_genre( + cq: CallbackQuery, + bot: AutoSend, + genre_id: u32, +) -> BotHandlerInternal { + let allowed_langs = get_user_or_default_lang_codes(cq.from.id).await; + + let item = book_library::get_random_book_by_genre(allowed_langs, Some(genre_id)).await; + + get_random_item_handler_internal(cq, bot, item).await +} + +pub fn get_random_hander() -> crate::bots::BotHandler { + dptree::entry() + .branch( + Update::filter_message() + .branch( + dptree::entry() + .filter_command::() + .endpoint(|message, command, bot| async { + match command { + RandomCommand::Random => random_handler(message, bot).await, + } + }) + ) + ) + .branch( + Update::filter_callback_query() + .chain(filter_callback_query::()) + .endpoint(|cq: CallbackQuery, callback_data: RandomCallbackData, bot: AutoSend| async move { + match callback_data { + RandomCallbackData::RandomBook => get_random_item_handler(cq, bot, book_library::get_random_book).await, + RandomCallbackData::RandomAuthor => get_random_item_handler(cq, bot, book_library::get_random_author).await, + RandomCallbackData::RandomSequence => get_random_item_handler(cq, bot, book_library::get_random_sequence).await, + RandomCallbackData::RandomBookByGenreRequest => get_genre_metas_handler(cq, bot).await, + RandomCallbackData::Genres { index } => get_genres_by_meta_handler(cq, bot, index).await, + RandomCallbackData::RandomBookByGenre { id } => get_random_book_by_genre(cq, bot, id).await, + } + }) + ) +} diff --git a/src/bots/approved_bot/modules/search.rs b/src/bots/approved_bot/modules/search.rs new file mode 100644 index 0000000..fca442c --- /dev/null +++ b/src/bots/approved_bot/modules/search.rs @@ -0,0 +1,281 @@ +use std::str::FromStr; + +use regex::Regex; +use strum_macros::EnumIter; +use teloxide::{ + prelude::*, + types::{InlineKeyboardButton, InlineKeyboardMarkup}, +}; + +use crate::bots::{ + approved_bot::{ + services::{ + book_library::{ + formaters::Format, search_author, search_book, search_sequence, search_translator, + types::Page, + }, + user_settings::get_user_or_default_lang_codes, + }, + tools::filter_callback_query, + }, + BotHandlerInternal, +}; + +use super::utils::{generic_get_pagination_keyboard, GetPaginationCallbackData}; + +#[derive(Clone, EnumIter)] +pub enum SearchCallbackData { + SearchBook { page: u32 }, + SearchAuthors { page: u32 }, + SearchSequences { page: u32 }, + SearchTranslators { page: u32 }, +} + +impl ToString for SearchCallbackData { + fn to_string(&self) -> String { + match self { + SearchCallbackData::SearchBook { page } => format!("sb_{page}"), + SearchCallbackData::SearchAuthors { page } => format!("sa_{page}"), + SearchCallbackData::SearchSequences { page } => format!("ss_{page}"), + SearchCallbackData::SearchTranslators { page } => format!("st_{page}"), + } + } +} + +impl FromStr for SearchCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + let re = Regex::new(r"^(?Ps[a|b|s|t])_(?P\d+)$").unwrap(); + + let caps = re.captures(s); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let search_type = &caps["search_type"]; + let page: u32 = caps["page"].parse::().unwrap(); + + match search_type { + "sb" => Ok(SearchCallbackData::SearchBook { page }), + "sa" => Ok(SearchCallbackData::SearchAuthors { page }), + "ss" => Ok(SearchCallbackData::SearchSequences { page }), + "st" => Ok(SearchCallbackData::SearchTranslators { page }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +impl GetPaginationCallbackData for SearchCallbackData { + fn get_pagination_callback_data(&self, target_page: u32) -> String { + match self { + SearchCallbackData::SearchBook { .. } => { + SearchCallbackData::SearchBook { page: target_page } + } + SearchCallbackData::SearchAuthors { .. } => { + SearchCallbackData::SearchAuthors { page: target_page } + } + SearchCallbackData::SearchSequences { .. } => { + SearchCallbackData::SearchSequences { page: target_page } + } + SearchCallbackData::SearchTranslators { .. } => { + SearchCallbackData::SearchTranslators { page: target_page } + } + } + .to_string() + } +} + +fn get_query(cq: CallbackQuery) -> Option { + cq.message + .map(|message| { + message + .reply_to_message() + .map(|reply_to_message| { + reply_to_message + .text() + .map(|text| text.replace('/', "").replace('&', "").replace('?', "")) + }) + .unwrap_or(None) + }) + .unwrap_or(None) +} + +async fn generic_search_pagination_handler( + cq: CallbackQuery, + bot: AutoSend, + search_data: SearchCallbackData, + items_getter: fn(query: String, page: u32, allowed_langs: Vec) -> Fut, +) -> BotHandlerInternal +where + T: Format + Clone, + Fut: std::future::Future, Box>>, +{ + let chat_id = cq.message.as_ref().map(|message| message.chat.id); + let user_id = cq + .message + .as_ref() + .map(|message| message.from().map(|from| from.id)) + .unwrap_or(None); + let message_id = cq.message.as_ref().map(|message| message.id); + let query = get_query(cq); + + let (chat_id, user_id, query, message_id) = match (chat_id, user_id, query, message_id) { + (Some(chat_id), Some(user_id), Some(query), Some(message_id)) => { + (chat_id, user_id, query, message_id) + } + _ => { + return match chat_id { + Some(v) => match bot.send_message(v, "Повторите поиск сначала").send().await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + }, + None => return Ok(()), + } + } + }; + + let allowed_langs = get_user_or_default_lang_codes(user_id).await; + + let page = match search_data { + SearchCallbackData::SearchBook { page } => page, + SearchCallbackData::SearchAuthors { page } => page, + SearchCallbackData::SearchSequences { page } => page, + SearchCallbackData::SearchTranslators { page } => page, + }; + + let mut items_page = match items_getter(query.clone(), page, allowed_langs.clone()).await { + Ok(v) => v, + Err(err) => { + match bot + .send_message(chat_id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + return Err(err); + } + }; + + if items_page.total_pages == 0 { + let message_text = match search_data { + SearchCallbackData::SearchBook { .. } => "Книги не найдены!", + SearchCallbackData::SearchAuthors { .. } => "Авторы не найдены!", + SearchCallbackData::SearchSequences { .. } => "Серии не найдены!", + SearchCallbackData::SearchTranslators { .. } => "Переводчики не найдены!", + }; + + match bot.send_message(chat_id, message_text).send().await { + Ok(_) => (), + Err(err) => return Err(Box::new(err)), + }; + }; + + if page > items_page.total_pages { + items_page = match items_getter( + query.clone(), + items_page.total_pages, + allowed_langs.clone(), + ) + .await + { + Ok(v) => v, + Err(err) => { + match bot + .send_message(chat_id, "Ошибка! Попробуйте позже :(") + .send() + .await + { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + return Err(err); + } + }; + } + + let formated_items = items_page.format_items(); + + let total_pages = items_page.total_pages; + + let footer = format!("\n\nСтраница {page}/{total_pages}"); + let message_text = format!("{formated_items}{footer}"); + + let keyboard = generic_get_pagination_keyboard(page, total_pages, search_data, true); + + match bot + .edit_message_text(chat_id, message_id, message_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn message_handler(message: Message, bot: AutoSend) -> BotHandlerInternal { + let message_text = "Что ищем?"; + + let keyboard = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![InlineKeyboardButton { + text: "Книгу".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + (SearchCallbackData::SearchBook { page: 1 }).to_string(), + ), + }], + vec![InlineKeyboardButton { + text: "Автора".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + (SearchCallbackData::SearchAuthors { page: 1 }).to_string(), + ), + }], + vec![InlineKeyboardButton { + text: "Серию".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + (SearchCallbackData::SearchSequences { page: 1 }).to_string(), + ), + }], + vec![InlineKeyboardButton { + text: "Переводчика".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + (SearchCallbackData::SearchTranslators { page: 1 }).to_string(), + ), + }], + ], + }; + + match bot + .send_message(message.chat.id, message_text) + .reply_to_message_id(message.id) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_search_hanlder() -> crate::bots::BotHandler { + dptree::entry().branch( + Update::filter_message() + .endpoint(|message, bot| async move { message_handler(message, bot).await }), + ).branch( + Update::filter_callback_query() + .chain(filter_callback_query::()) + .endpoint(|cq: CallbackQuery, callback_data: SearchCallbackData, bot: AutoSend| async move { + match callback_data { + SearchCallbackData::SearchBook { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_book).await, + SearchCallbackData::SearchAuthors { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_author).await, + SearchCallbackData::SearchSequences { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_sequence).await, + SearchCallbackData::SearchTranslators { .. } => generic_search_pagination_handler(cq, bot, callback_data, search_translator).await, + } + }) + ) +} diff --git a/src/bots/approved_bot/modules/settings.rs b/src/bots/approved_bot/modules/settings.rs new file mode 100644 index 0000000..39a3c79 --- /dev/null +++ b/src/bots/approved_bot/modules/settings.rs @@ -0,0 +1,217 @@ +use std::{collections::HashSet, str::FromStr, vec}; + +use crate::bots::{ + approved_bot::{ + services::user_settings::{ + create_or_update_user_settings, get_langs, get_user_or_default_lang_codes, Lang, + }, + tools::filter_callback_query, + }, + BotHandlerInternal, +}; + +use regex::Regex; +use teloxide::{ + prelude::*, + types::{InlineKeyboardButton, InlineKeyboardMarkup, Me}, + utils::command::BotCommands, +}; + +#[derive(BotCommands, Clone)] +#[command(rename = "lowercase")] +enum SettingsCommand { + Settings, +} + +#[derive(Clone)] +enum SettingsCallbackData { + LangSettings, + LangOn { code: String }, + LangOff { code: String }, +} + +impl FromStr for SettingsCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + if s == SettingsCallbackData::LangSettings.to_string().as_str() { + return Ok(SettingsCallbackData::LangSettings); + } + + let re = Regex::new(r"^lang_(?P(off)|(on))_(?P[a-zA-z]+)$").unwrap(); + + let caps = re.captures(s); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let action = &caps["action"]; + let code = caps["code"].to_string(); + + match action { + "on" => Ok(SettingsCallbackData::LangOn { code }), + "off" => Ok(SettingsCallbackData::LangOff { code }), + _ => Err(strum::ParseError::VariantNotFound), + } + } +} + +impl ToString for SettingsCallbackData { + fn to_string(&self) -> String { + match self { + SettingsCallbackData::LangSettings => "lang_settings".to_string(), + SettingsCallbackData::LangOn { code } => format!("lang_on_{code}"), + SettingsCallbackData::LangOff { code } => format!("lang_off_{code}"), + } + } +} + +async fn settings_handler(message: Message, bot: AutoSend) -> BotHandlerInternal { + let keyboard = InlineKeyboardMarkup { + inline_keyboard: vec![vec![InlineKeyboardButton { + text: "Языки".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + SettingsCallbackData::LangSettings.to_string(), + ), + }]], + }; + + match bot + .send_message(message.chat.id, "Настройки") + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +fn get_lang_keyboard(all_langs: Vec, allowed_langs: HashSet) -> InlineKeyboardMarkup { + let buttons = all_langs + .into_iter() + .map(|lang| { + let (emoji, callback_data) = match allowed_langs.contains(&lang.code) { + true => ( + "🟢".to_string(), + SettingsCallbackData::LangOff { code: lang.code }.to_string(), + ), + false => ( + "🔴".to_string(), + SettingsCallbackData::LangOn { code: lang.code }.to_string(), + ), + }; + + vec![InlineKeyboardButton { + text: format!("{emoji} {}", lang.label), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(callback_data), + }] + }) + .collect(); + + InlineKeyboardMarkup { + inline_keyboard: buttons, + } +} + +async fn settings_callback_handler( + cq: CallbackQuery, + bot: AutoSend, + callback_data: SettingsCallbackData, + me: Me, +) -> BotHandlerInternal { + let message = match cq.message { + Some(v) => v, + None => return Ok(()), // TODO: alert + }; + + let user = match message.from() { + Some(v) => v, + None => return Ok(()), // TODO: alert + }; + + let allowed_langs = get_user_or_default_lang_codes(user.id).await; + + let mut allowed_langs_set: HashSet = HashSet::new(); + allowed_langs.clone().into_iter().for_each(|v| { + allowed_langs_set.insert(v); + }); + + match callback_data { + SettingsCallbackData::LangSettings => (), + SettingsCallbackData::LangOn { code } => { + allowed_langs_set.insert(code); + } + SettingsCallbackData::LangOff { code } => { + allowed_langs_set.remove(&code); + } + }; + + if allowed_langs_set.len() == 0 { + return match bot + .answer_callback_query(cq.id) + .text("Должен быть активен, хотя бы один язык!") + .show_alert(true) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + }; + } + + match create_or_update_user_settings( + user.id, + user.last_name.clone().unwrap_or("".to_string()), + user.first_name.clone(), + user.username.clone().unwrap_or("".to_string()), + me.username.clone().unwrap(), + allowed_langs_set.clone().into_iter().collect(), + ) + .await + { + Ok(_) => (), + Err(err) => return Err(err), // TODO: err + }; + + let all_langs = match get_langs().await { + Ok(v) => v, + Err(err) => return Err(err), + }; + + let keyboard = get_lang_keyboard(all_langs, allowed_langs_set); + + match bot + .edit_message_reply_markup(message.chat.id, message.id) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_settings_handler() -> crate::bots::BotHandler { + dptree::entry() + .branch( + Update::filter_message().branch( + dptree::entry() + .filter_command::() + .endpoint(|message, bot| async move { settings_handler(message, bot).await }), + ), + ) + .branch( + Update::filter_callback_query() + .chain(filter_callback_query::()) + .endpoint( + |cq: CallbackQuery, + bot: AutoSend, + callback_data: SettingsCallbackData, + me: Me| async move { + settings_callback_handler(cq, bot, callback_data, me).await + }, + ), + ) +} diff --git a/src/bots/approved_bot/modules/support.rs b/src/bots/approved_bot/modules/support.rs new file mode 100644 index 0000000..cbf56f4 --- /dev/null +++ b/src/bots/approved_bot/modules/support.rs @@ -0,0 +1,50 @@ +use crate::bots::BotHandlerInternal; + +use teloxide::{ + prelude::*, + types::{InlineKeyboardButton, InlineKeyboardMarkup}, + utils::command::BotCommands, +}; + +#[derive(BotCommands, Clone)] +#[command(rename = "lowercase")] +enum SupportCommand { + Support, +} + +pub async fn support_command_handler(message: Message, bot: AutoSend) -> BotHandlerInternal { + const MESSAGE_TEXT: &str = " +[Лицензии](https://github.com/flibusta-apps/book_bot/blob/main/LICENSE.md) + +[Исходный код](https://github.com/flibusta-apps) + "; + + let keyboard = InlineKeyboardMarkup { + inline_keyboard: vec![vec![InlineKeyboardButton { + kind: teloxide::types::InlineKeyboardButtonKind::Url( + url::Url::parse("https://kurbezz.github.io/Kurbezz/").unwrap(), + ), + text: String::from("☕️ Поддержать разработчика"), + }]], + }; + + match bot + .send_message(message.chat.id, MESSAGE_TEXT) + .parse_mode(teloxide::types::ParseMode::MarkdownV2) + .reply_markup(keyboard) + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_support_handler() -> crate::bots::BotHandler { + dptree::entry().branch( + Update::filter_message().branch( + dptree::entry().filter_command::().endpoint( + |message, bot| async move { support_command_handler(message, bot).await }, + ), + ), + ) +} diff --git a/src/bots/approved_bot/modules/update_history.rs b/src/bots/approved_bot/modules/update_history.rs new file mode 100644 index 0000000..6ba68f4 --- /dev/null +++ b/src/bots/approved_bot/modules/update_history.rs @@ -0,0 +1,227 @@ +use chrono::{prelude::*, Duration}; +use dateparser::parse; + +use std::{str::FromStr, vec}; + +use crate::bots::{ + approved_bot::{services::book_library::get_uploaded_books, tools::filter_callback_query}, + BotHandlerInternal, +}; + +use regex::Regex; +use teloxide::{ + prelude::*, + types::{InlineKeyboardButton, InlineKeyboardMarkup}, + utils::command::BotCommands, +}; + +use super::utils::{generic_get_pagination_keyboard, GetPaginationCallbackData}; + +#[derive(BotCommands, Clone)] +#[command(rename = "snake_case")] +enum UpdateLogCommand { + UpdateLog, +} + +#[derive(Clone, Copy)] +struct UpdateLogCallbackData { + from: Date, + to: Date, + page: u32, +} + +impl FromStr for UpdateLogCallbackData { + type Err = strum::ParseError; + + fn from_str(s: &str) -> Result { + let re = Regex::new( + r"^update_log_(?P\d{4}-\d{2}-\d{2})_(?P\d{4}-\d{2}-\d{2})_(?P\d+)$", + ) + .unwrap(); + + let caps = re.captures(s); + let caps = match caps { + Some(v) => v, + None => return Err(strum::ParseError::VariantNotFound), + }; + + let from: Date = parse(&caps["from"]).unwrap().date(); + let to: Date = parse(&caps["to"]).unwrap().date(); + let page: u32 = caps["page"].parse().unwrap(); + + Ok(UpdateLogCallbackData { from, to, page }) + } +} + +impl ToString for UpdateLogCallbackData { + fn to_string(&self) -> String { + let date_format = "%Y-%m-%d"; + + let from = self.from.format(date_format); + let to = self.to.format(date_format); + let page = self.page; + + format!("update_log_{from}_{to}_{page}") + } +} + +impl GetPaginationCallbackData for UpdateLogCallbackData { + fn get_pagination_callback_data(&self, target_page: u32) -> String { + let UpdateLogCallbackData { from, to, .. } = self; + UpdateLogCallbackData { + from: from.clone(), + to: to.clone(), + page: target_page, + } + .to_string() + } +} + +async fn update_log_command(message: Message, bot: AutoSend) -> BotHandlerInternal { + let now = Utc::today(); + let d3 = now - Duration::days(3); + let d7 = now - Duration::days(7); + let d30 = now - Duration::days(30); + + let keyboard = InlineKeyboardMarkup { + inline_keyboard: vec![ + vec![InlineKeyboardButton { + text: "За 3 дня".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + UpdateLogCallbackData { + from: d3, + to: now, + page: 1, + } + .to_string(), + ), + }], + vec![InlineKeyboardButton { + text: "За 7 дней".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + UpdateLogCallbackData { + from: d7, + to: now, + page: 1, + } + .to_string(), + ), + }], + vec![InlineKeyboardButton { + text: "За 30 дней".to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData( + UpdateLogCallbackData { + from: d30, + to: now, + page: 1, + } + .to_string(), + ), + }], + ], + }; + + match bot + .send_message(message.chat.id, "Обновление каталога:") + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +async fn update_log_pagination_handler( + cq: CallbackQuery, + bot: AutoSend, + update_callback_data: UpdateLogCallbackData, +) -> BotHandlerInternal { + let message = match cq.message { + Some(v) => v, + None => return Ok(()), // TODO: send notification + }; + + let from = update_callback_data.from.format("%d.%m.%Y"); + let to = update_callback_data.to.format("%d.%m.%Y"); + + let header = format!("Обновление каталога ({from} - {to}):\n\n"); + + let mut items_page = match get_uploaded_books( + update_callback_data.page, + update_callback_data.from.format("%Y-%m-%d").to_string(), + update_callback_data.to.format("%Y-%m-%d").to_string(), + ) + .await + { + Ok(v) => v, + Err(err) => return Err(err), + }; + + if items_page.total_pages == 0 { + return match bot + .send_message(message.chat.id, "Нет новых книг за этот период.") + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + }; + } + + if update_callback_data.page > items_page.total_pages { + items_page = match get_uploaded_books( + items_page.total_pages, + update_callback_data.from.format("YYYY-MM-DD").to_string(), + update_callback_data.to.format("YYYY-MM-DD").to_string(), + ) + .await + { + Ok(v) => v, + Err(err) => return Err(err), + }; + } + + let formated_items = items_page.format_items(); + + let page = update_callback_data.page; + let total_pages = items_page.total_pages; + let footer = format!("\n\nСтраница {page}/{total_pages}"); + + let message_text = format!("{header}{formated_items}{footer}"); + + let keyboard = generic_get_pagination_keyboard(1, total_pages, update_callback_data, true); + match bot + .edit_message_text(message.chat.id, message.id, message_text) + .reply_markup(keyboard) + .send() + .await + { + Ok(_) => Ok(()), + Err(err) => Err(Box::new(err)), + } +} + +pub fn get_update_log_handler() -> crate::bots::BotHandler { + dptree::entry() + .branch( + Update::filter_message().branch( + dptree::entry() + .filter_command::() + .endpoint(|message, bot| async move { update_log_command(message, bot).await }), + ), + ) + .branch( + Update::filter_callback_query().branch( + dptree::entry() + .chain(filter_callback_query::()) + .endpoint( + |cq: CallbackQuery, + bot: AutoSend, + update_log_data: UpdateLogCallbackData| async move { + update_log_pagination_handler(cq, bot, update_log_data).await + }, + ), + ), + ) +} diff --git a/src/bots/approved_bot/modules/utils.rs b/src/bots/approved_bot/modules/utils.rs new file mode 100644 index 0000000..2edd74a --- /dev/null +++ b/src/bots/approved_bot/modules/utils.rs @@ -0,0 +1,114 @@ +use teloxide::{dptree, prelude::*, types::*}; + +pub trait CommandParse { + fn parse(s: &str, bot_name: &str) -> Result; +} + +pub fn filter_command() -> crate::bots::BotHandler +where + Output: CommandParse + Send + Sync + 'static, +{ + dptree::entry().chain(dptree::filter_map(move |message: Message, me: Me| { + let bot_name = me.user.username.expect("Bots must have a username"); + message + .text() + .and_then(|text| Output::parse(text, &bot_name).ok()) + })) +} + +pub enum PaginationDelta { + OneMinus, + OnePlus, + FiveMinus, + FivePlus, +} + +pub trait GetPaginationCallbackData { + fn get_pagination_callback_data(&self, target_page: u32) -> String; +} + +pub fn generic_get_pagination_button( + target_page: u32, + delta: PaginationDelta, + callback_data: &T, +) -> InlineKeyboardButton +where + T: GetPaginationCallbackData, +{ + let text = match delta { + PaginationDelta::OneMinus => "<", + PaginationDelta::OnePlus => ">", + PaginationDelta::FiveMinus => "< 5 <", + PaginationDelta::FivePlus => "> 5 >", + }; + + let callback_data = callback_data.get_pagination_callback_data(target_page); + + InlineKeyboardButton { + text: text.to_string(), + kind: teloxide::types::InlineKeyboardButtonKind::CallbackData(callback_data), + } +} + +pub fn generic_get_pagination_keyboard( + page: u32, + total_pages: u32, + search_data: T, + with_five: bool, +) -> InlineKeyboardMarkup +where + T: GetPaginationCallbackData, +{ + let buttons: Vec> = { + let t_page: i64 = page.into(); + + let mut result: Vec> = vec![]; + + let mut one_page_row: Vec = vec![]; + + if t_page - 1 > 0 { + one_page_row.push(generic_get_pagination_button( + page - 1, + PaginationDelta::OneMinus, + &search_data, + )) + } + if t_page + 1 <= total_pages.into() { + one_page_row.push(generic_get_pagination_button( + page + 1, + PaginationDelta::OnePlus, + &search_data, + )) + } + if one_page_row.len() != 0 { + result.push(one_page_row); + } + + if with_five { + let mut five_page_row: Vec = vec![]; + if t_page - 5 > 0 { + five_page_row.push(generic_get_pagination_button( + page - 5, + PaginationDelta::FiveMinus, + &search_data, + )) + } + if t_page + 1 <= total_pages.into() { + five_page_row.push(generic_get_pagination_button( + page + 5, + PaginationDelta::FivePlus, + &search_data, + )) + } + if five_page_row.len() != 0 { + result.push(five_page_row); + } + } + + result + }; + + InlineKeyboardMarkup { + inline_keyboard: buttons, + } +} diff --git a/src/bots/approved_bot/services/book_cache/mod.rs b/src/bots/approved_bot/services/book_cache/mod.rs new file mode 100644 index 0000000..bccd54e --- /dev/null +++ b/src/bots/approved_bot/services/book_cache/mod.rs @@ -0,0 +1,112 @@ +use crate::{bots::approved_bot::modules::download::DownloadData, config}; + +use self::types::{CachedMessage, DownloadFile}; + +pub mod types; + +pub async fn get_cached_message( + download_data: &DownloadData, +) -> Result> { + let DownloadData { format, id } = download_data; + + let client = reqwest::Client::new(); + let response = client + .get(format!( + "{}/api/v1/{id}/{format}", + &config::CONFIG.cache_server_url + )) + .header("Authorization", &config::CONFIG.cache_server_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.json::().await { + Ok(v) => Ok(v), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn clear_book_cache( + download_data: &DownloadData, +) -> Result<(), Box> { + let DownloadData { format, id } = download_data; + + let client = reqwest::Client::new(); + let response = client + .delete(format!( + "{}/api/v1/{id}/{format}", + &config::CONFIG.cache_server_url + )) + .header("Authorization", &config::CONFIG.cache_server_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.error_for_status() { + Ok(_) => Ok(()), + Err(err) => return Err(Box::new(err)), + } +} + +pub async fn download_file( + download_data: &DownloadData, +) -> Result> { + let DownloadData { format, id } = download_data; + + let client = reqwest::Client::new(); + let response = client + .get(format!( + "{}/api/v1/download/{id}/{format}", + &config::CONFIG.cache_server_url + )) + .header("Authorization", &config::CONFIG.cache_server_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.error_for_status() { + Ok(response) => { + let headers = response.headers(); + let filename = headers + .get("content-disposition") + .unwrap() + .to_str() + .unwrap() + .replace('"', "") + .split("filename=") + .collect::>() + .get(1) + .unwrap() + .to_string(); + let caption = std::str::from_utf8( + &base64::decode(headers.get("x-caption-b64").unwrap()).unwrap(), + ) + .unwrap() + .to_string(); + + Ok(DownloadFile { + response, + filename, + caption, + }) + } + Err(err) => return Err(Box::new(err)), + } +} diff --git a/src/bots/approved_bot/services/book_cache/types.rs b/src/bots/approved_bot/services/book_cache/types.rs new file mode 100644 index 0000000..67f72aa --- /dev/null +++ b/src/bots/approved_bot/services/book_cache/types.rs @@ -0,0 +1,13 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Clone)] +pub struct CachedMessage { + pub message_id: i32, + pub chat_id: i64, +} + +pub struct DownloadFile { + pub response: reqwest::Response, + pub filename: String, + pub caption: String, +} diff --git a/src/bots/approved_bot/services/book_library/formaters.rs b/src/bots/approved_bot/services/book_library/formaters.rs new file mode 100644 index 0000000..5b232df --- /dev/null +++ b/src/bots/approved_bot/services/book_library/formaters.rs @@ -0,0 +1,299 @@ +use super::types::{Author, AuthorBook, Book, SearchBook, Sequence, Translator, TranslatorBook}; + +pub trait Format { + fn format(&self) -> String; +} + +impl Format for Book { + fn format(&self) -> String { + let book_title = { + let Book { title, lang, .. } = self; + format!("📖 {title} | {lang}\n") + }; + + let pages_count = match self.pages { + Some(1) | None => "".to_string(), + Some(v) => format!("[ {v}с. ]\n\n"), + }; + + let annotations = match self.annotation_exists { + true => { + let Book { id, .. } = self; + format!("📝 Аннотация: /b_an_{id}\n\n") + } + false => "".to_string(), + }; + + let authors = match self.authors.len() != 0 { + true => { + let formated_authors = self + .authors + .clone() + .into_iter() + .map(|author| author.format_author()) + .collect::>() + .join("\n"); + format!("Авторы:\n{formated_authors}\n\n") + } + false => "".to_string(), + }; + + let translators = match self.translators.len() != 0 { + true => { + let formated_translators = self + .translators + .clone() + .into_iter() + .map(|translator| translator.format_translator()) + .collect::>() + .join("\n"); + format!("Переводчики:\n{formated_translators}\n\n") + } + false => "".to_string(), + }; + + let sequences = match self.sequences.len() != 0 { + true => { + let formated_sequences: String = self + .sequences + .clone() + .into_iter() + .map(|sequence| sequence.format()) + .collect::>() + .join("\n"); + format!("Серии:\n{formated_sequences}\n\n") + } + false => "".to_string(), + }; + + let genres = match self.genres.len() != 0 { + true => { + let formated_genres: String = self + .genres + .clone() + .into_iter() + .map(|genre| genre.format()) + .collect::>() + .join("\n"); + format!("Жанры:\n{formated_genres}\n\n") + } + false => "".to_string(), + }; + + let links: String = self + .available_types + .clone() + .into_iter() + .map(|a_type| { + let Book { id, .. } = self; + format!("📥 {a_type}: /d_{a_type}_{id}") + }) + .collect::>() + .join("\n"); + let download_links = format!("Скачать:\n{links}"); + + format!("{book_title}{pages_count}{annotations}{authors}{translators}{sequences}{genres}{download_links}") + } +} + +impl Format for Author { + fn format(&self) -> String { + let Author { + id, + last_name, + first_name, + middle_name, + .. + } = self; + + let title = format!("👤 {last_name} {first_name} {middle_name}\n"); + let link = format!("/a_{id}\n"); + let annotation = match self.annotation_exists { + true => format!("📝 Аннотация: /a_an_{id}"), + false => "".to_string(), + }; + + format!("{title}{link}{annotation}") + } +} + +impl Format for Sequence { + fn format(&self) -> String { + let Sequence { id, name, .. } = self; + + let title = format!("📚 {name}\n"); + let link = format!("/s_{id}"); + + format!("{title}{link}") + } +} + +impl Format for SearchBook { + fn format(&self) -> String { + let book_title = { + let SearchBook { title, lang, .. } = self; + format!("📖 {title} | {lang}\n") + }; + + let annotations = match self.annotation_exists { + true => { + let SearchBook { id, .. } = self; + format!("📝 Аннотация: /b_an_{id}\n") + } + false => "".to_string(), + }; + + let authors = match self.authors.len() != 0 { + true => { + let formated_authors = self + .authors + .clone() + .into_iter() + .map(|author| author.format_author()) + .collect::>() + .join("\n"); + format!("Авторы:\n{formated_authors}\n") + } + false => "".to_string(), + }; + + let translators = match self.translators.len() != 0 { + true => { + let formated_translators = self + .translators + .clone() + .into_iter() + .map(|translator| translator.format_translator()) + .collect::>() + .join("\n"); + format!("Переводчики:\n{formated_translators}\n") + } + false => "".to_string(), + }; + + let links: String = self + .available_types + .clone() + .into_iter() + .map(|a_type| { + let SearchBook { id, .. } = self; + format!("📥 {a_type}: /d_{a_type}_{id}") + }) + .collect::>() + .join("\n"); + let download_links = format!("Скачать:\n{links}"); + + format!("{book_title}{annotations}{authors}{translators}{download_links}") + } +} + +impl Format for Translator { + fn format(&self) -> String { + let Translator { + id, + last_name, + first_name, + middle_name, + .. + } = self; + + let title = format!("👤 {last_name} {first_name} {middle_name}\n"); + let link = format!("/t_{id}\n"); + let annotation = match self.annotation_exists { + true => format!("📝 Аннотация: /a_an_{id}"), + false => "".to_string(), + }; + + format!("{title}{link}{annotation}") + } +} + +impl Format for AuthorBook { + fn format(&self) -> String { + let book_title = { + let AuthorBook { title, lang, .. } = self; + format!("📖 {title} | {lang}\n") + }; + + let annotations = match self.annotation_exists { + true => { + let AuthorBook { id, .. } = self; + format!("📝 Аннотация: /b_an_{id}\n") + } + false => "".to_string(), + }; + + let translators = match self.translators.len() != 0 { + true => { + let formated_translators = self + .translators + .clone() + .into_iter() + .map(|translator| translator.format_translator()) + .collect::>() + .join("\n"); + format!("Переводчики:\n{formated_translators}\n") + } + false => "".to_string(), + }; + + let links: String = self + .available_types + .clone() + .into_iter() + .map(|a_type| { + let AuthorBook { id, .. } = self; + format!("📥 {a_type}: /d_{a_type}_{id}") + }) + .collect::>() + .join("\n"); + let download_links = format!("Скачать:\n{links}"); + + format!("{book_title}{annotations}{translators}{download_links}") + } +} + +impl Format for TranslatorBook { + fn format(&self) -> String { + let book_title = { + let TranslatorBook { title, lang, .. } = self; + format!("📖 {title} | {lang}\n") + }; + + let annotations = match self.annotation_exists { + true => { + let TranslatorBook { id, .. } = self; + format!("📝 Аннотация: /b_an_{id}\n") + } + false => "".to_string(), + }; + + let authors = match self.authors.len() != 0 { + true => { + let formated_authors = self + .authors + .clone() + .into_iter() + .map(|author| author.format_author()) + .collect::>() + .join("\n"); + format!("Авторы:\n{formated_authors}\n") + } + false => "".to_string(), + }; + + let links: String = self + .available_types + .clone() + .into_iter() + .map(|a_type| { + let TranslatorBook { id, .. } = self; + format!("📥 {a_type}: /d_{a_type}_{id}") + }) + .collect::>() + .join("\n"); + let download_links = format!("Скачать:\n{links}"); + + format!("{book_title}{annotations}{authors}{download_links}") + } +} diff --git a/src/bots/approved_bot/services/book_library/mod.rs b/src/bots/approved_bot/services/book_library/mod.rs new file mode 100644 index 0000000..5ac4cb1 --- /dev/null +++ b/src/bots/approved_bot/services/book_library/mod.rs @@ -0,0 +1,217 @@ +pub mod formaters; +pub mod types; + +use serde::de::DeserializeOwned; + +use crate::config; + +fn get_allowed_langs_params(allowed_langs: Vec) -> Vec<(&'static str, String)> { + allowed_langs + .into_iter() + .map(|lang| ("allowed_langs", lang)) + .collect() +} + +async fn _make_request( + url: &str, + params: Vec<(&str, String)>, +) -> Result> +where + T: DeserializeOwned, +{ + let client = reqwest::Client::new(); + let response = client + .get(format!("{}{}", &config::CONFIG.book_server_url, url)) + .query(¶ms) + .header("Authorization", &config::CONFIG.book_server_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.json::().await { + Ok(v) => Ok(v), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn get_random_book_by_genre( + allowed_langs: Vec, + genre: Option, +) -> Result> { + let mut params: Vec<(&str, String)> = get_allowed_langs_params(allowed_langs); + + match genre { + Some(v) => params.push(("genre", v.to_string())), + None => (), + } + + _make_request("/api/v1/books/random", params).await +} + +pub async fn get_random_book( + allowed_langs: Vec, +) -> Result> { + get_random_book_by_genre(allowed_langs, None).await +} + +pub async fn get_random_author( + allowed_langs: Vec, +) -> Result> { + let params: Vec<(&str, String)> = get_allowed_langs_params(allowed_langs); + + _make_request("/api/v1/authors/random", params).await +} + +pub async fn get_random_sequence( + allowed_langs: Vec, +) -> Result> { + let params = get_allowed_langs_params(allowed_langs); + + _make_request("/api/v1/sequences/random", params).await +} + +pub async fn get_genre_metas() -> Result, Box> { + _make_request("/api/v1/genres/metas", vec![]).await +} + +pub async fn get_genres( + meta: String, +) -> Result, Box> { + let params = vec![("meta", meta)]; + + _make_request("/api/v1/genres/", params).await +} + +const PAGE_SIZE: &str = "7"; + +pub async fn search_book( + query: String, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/books/search/{query}").as_str(), params).await +} + +pub async fn search_author( + query: String, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/authors/search/{query}").as_str(), params).await +} + +pub async fn search_sequence( + query: String, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/sequences/search/{query}").as_str(), params).await +} + +pub async fn search_translator( + query: String, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request( + format!("/api/v1/translators/search/{query}").as_str(), + params, + ) + .await +} + +pub async fn get_book_annotation( + id: u32, +) -> Result> { + _make_request(format!("/api/v1/books/{id}/annotation").as_str(), vec![]).await +} + +pub async fn get_author_annotation( + id: u32, +) -> Result> { + _make_request(format!("/api/v1/authors/{id}/annotation").as_str(), vec![]).await +} + +pub async fn get_author_books( + id: u32, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/authors/{id}/books").as_str(), params).await +} + +pub async fn get_translator_books( + id: u32, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/translators/{id}/books").as_str(), params).await +} + +pub async fn get_sequence_books( + id: u32, + page: u32, + allowed_langs: Vec, +) -> Result, Box> { + let mut params = get_allowed_langs_params(allowed_langs); + + params.push(("page", page.to_string())); + params.push(("size", PAGE_SIZE.to_string())); + + _make_request(format!("/api/v1/sequences/{id}/books").as_str(), params).await +} + +pub async fn get_uploaded_books( + page: u32, + uploaded_gte: String, + uploaded_lte: String, +) -> Result, Box> { + let params = vec![ + ("page", page.to_string()), + ("size", PAGE_SIZE.to_string()), + ("uploaded_gte", uploaded_gte), + ("uploaded_lte", uploaded_lte), + ("is_deleted", "false".to_string()), + ]; + + _make_request("/api/v1/books/", params).await +} diff --git a/src/bots/approved_bot/services/book_library/types.rs b/src/bots/approved_bot/services/book_library/types.rs new file mode 100644 index 0000000..6710a3d --- /dev/null +++ b/src/bots/approved_bot/services/book_library/types.rs @@ -0,0 +1,182 @@ +use serde::Deserialize; + +use super::formaters::Format; + +#[derive(Deserialize, Debug, Clone)] +pub struct BookAuthor { + id: u32, + first_name: String, + last_name: String, + middle_name: String, +} + +impl BookAuthor { + pub fn format_author(&self) -> String { + let BookAuthor { + id, + last_name, + first_name, + middle_name, + } = self; + + format!("👤 {last_name} {first_name} {middle_name} /a_{id}") + } + + pub fn format_translator(&self) -> String { + let BookAuthor { + id, + first_name, + last_name, + middle_name, + } = self; + + format!("👤 {last_name} {first_name} {middle_name} /t_{id}") + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct BookGenre { + pub id: u32, + pub description: String, +} + +impl BookGenre { + pub fn format(&self) -> String { + format!("🗂 {}", self.description) + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Source { + // id: u32, +// name: String +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Book { + pub id: u32, + pub title: String, + pub lang: String, + // file_type: String, + pub available_types: Vec, + // uploaded: String, + pub annotation_exists: bool, + pub authors: Vec, + pub translators: Vec, + pub sequences: Vec, + pub genres: Vec, + // source: Source, + // remote_id: u32, + // id_deleted: bool, + pub pages: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Author { + pub id: u32, + pub last_name: String, + pub first_name: String, + pub middle_name: String, + pub annotation_exists: bool, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Translator { + pub id: u32, + pub last_name: String, + pub first_name: String, + pub middle_name: String, + pub annotation_exists: bool, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Sequence { + pub id: u32, + pub name: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Genre { + pub id: u32, + pub source: Source, + pub remote_id: u32, + pub code: String, + pub description: String, + pub meta: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct Page { + pub items: Vec, + pub total: u32, + pub page: u32, + pub size: u32, + pub total_pages: u32, +} + +impl Page +where + T: Format + Clone, +{ + pub fn format_items(&self) -> String { + self.items + .clone() + .into_iter() + .map(|book| book.format()) + .collect::>() + .join("\n\n\n") + } +} + +#[derive(Deserialize, Debug, Clone)] +pub struct SearchBook { + pub id: u32, + pub title: String, + pub lang: String, + // file_type: String, + pub available_types: Vec, + // uploaded: String, + pub annotation_exists: bool, + pub authors: Vec, + pub translators: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct BookAnnotation { + pub id: u32, + pub title: String, + pub text: String, + pub file: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct AuthorAnnotation { + pub id: u32, + pub title: String, + pub text: String, + pub file: Option, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct AuthorBook { + pub id: u32, + pub title: String, + pub lang: String, + // file_type: String, + pub available_types: Vec, + // uploaded: String, + pub annotation_exists: bool, + pub translators: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TranslatorBook { + pub id: u32, + pub title: String, + pub lang: String, + // file_type: String, + pub available_types: Vec, + // uploaded: String, + pub annotation_exists: bool, + pub authors: Vec, +} diff --git a/src/bots/approved_bot/services/mod.rs b/src/bots/approved_bot/services/mod.rs new file mode 100644 index 0000000..0869a96 --- /dev/null +++ b/src/bots/approved_bot/services/mod.rs @@ -0,0 +1,3 @@ +pub mod book_cache; +pub mod book_library; +pub mod user_settings; diff --git a/src/bots/approved_bot/services/user_settings/mod.rs b/src/bots/approved_bot/services/user_settings/mod.rs new file mode 100644 index 0000000..3173f67 --- /dev/null +++ b/src/bots/approved_bot/services/user_settings/mod.rs @@ -0,0 +1,126 @@ +use serde::Deserialize; +use serde_json::json; +use teloxide::types::UserId; + +use crate::config; + +#[derive(Deserialize, Debug, Clone)] +pub struct Lang { + // pub id: u32, + pub label: String, + pub code: String, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct UserSettings { + pub user_id: u64, + pub last_name: String, + pub first_name: String, + pub username: String, + pub source: String, + pub allowed_langs: Vec, +} + +pub async fn get_user_settings( + user_id: UserId, +) -> Result> { + let client = reqwest::Client::new(); + let response = client + .get(format!( + "{}/users/{}", + &config::CONFIG.user_settings_url, + user_id + )) + .header("Authorization", &config::CONFIG.user_settings_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.json::().await { + Ok(v) => Ok(v), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn get_user_or_default_lang_codes(user_id: UserId) -> Vec { + let default_lang_codes = vec![String::from("ru"), String::from("be"), String::from("uk")]; + + match get_user_settings(user_id).await { + Ok(v) => v.allowed_langs.into_iter().map(|lang| lang.code).collect(), + Err(_) => default_lang_codes, + } +} + +pub async fn create_or_update_user_settings( + user_id: UserId, + last_name: String, + first_name: String, + username: String, + source: String, + allowed_langs: Vec, +) -> Result> { + let body = json!({ + "user_id": user_id, + "last_name": last_name, + "first_name": first_name, + "username": username, + "source": source, + "allowed_langs": allowed_langs + }); + + let client = reqwest::Client::new(); + let response = client + .post(format!("{}/users/", &config::CONFIG.user_settings_url)) + .body(body.to_string()) + .header("Authorization", &config::CONFIG.user_settings_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.json::().await { + Ok(v) => Ok(v), + Err(err) => Err(Box::new(err)), + } +} + +pub async fn get_langs() -> Result, Box> { + let client = reqwest::Client::new(); + let response = client + .get(format!("{}/languages/", &config::CONFIG.user_settings_url)) + .header("Authorization", &config::CONFIG.user_settings_api_key) + .send() + .await; + + let response = match response { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + let response = match response.error_for_status() { + Ok(v) => v, + Err(err) => return Err(Box::new(err)), + }; + + match response.json::>().await { + Ok(v) => Ok(v), + Err(err) => Err(Box::new(err)), + } +} diff --git a/src/bots/approved_bot/tools.rs b/src/bots/approved_bot/tools.rs new file mode 100644 index 0000000..b639ddc --- /dev/null +++ b/src/bots/approved_bot/tools.rs @@ -0,0 +1,10 @@ +use teloxide::{dptree, types::CallbackQuery}; + +pub fn filter_callback_query() -> crate::bots::BotHandler +where + T: std::str::FromStr + Send + Sync + 'static, +{ + dptree::entry().chain(dptree::filter_map(move |cq: CallbackQuery| { + cq.data.and_then(|data| T::from_str(data.as_str()).ok()) + })) +} diff --git a/src/bots/factory/bots/approved/annotations.ts b/src/bots/factory/bots/approved/annotations.ts deleted file mode 100644 index 7893d84..0000000 --- a/src/bots/factory/bots/approved/annotations.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { Context } from "telegraf"; -import { AuthorAnnotation, BookAnnotation } from "./services/book_library"; - -import { isNormalText } from "./utils"; -import { getTextPaginationData } from './keyboard'; -import Sentry from '@/sentry'; -import { downloadImage } from "./services/downloader"; - - -export function getAnnotationHandler( - annotationGetter: (id: number) => Promise, - callbackData: string -): (ctx: Context) => Promise { - return async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const objId = ctx.message.text.split("@")[0].split('_')[2]; - - const annotation = await annotationGetter(parseInt(objId)); - - if (!annotation.file && !isNormalText(annotation.text)) { - await ctx.reply("Аннотация недоступна :("); - return; - } - - if (annotation.file) { - const imageData = await downloadImage(annotation.file); - - if (imageData !== null) { - try { - await ctx.telegram.sendPhoto(ctx.message.chat.id, { source: imageData }); - } catch (e) { - Sentry.captureException(e); - } - } - } - - if (!isNormalText(annotation.text)) return; - - const data = getTextPaginationData(`${callbackData}${objId}`, annotation.text, 0); - - try { - await ctx.reply(data.current, { - parse_mode: "HTML", - reply_markup: data.keyboard.reply_markup, - }); - } catch (e) { - Sentry.captureException(e, { - extra: { - message: data.current, - annotation, - objId - } - }) - } - } -} diff --git a/src/bots/factory/bots/approved/callback_data.ts b/src/bots/factory/bots/approved/callback_data.ts deleted file mode 100644 index 38200f5..0000000 --- a/src/bots/factory/bots/approved/callback_data.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const SETTINGS_LANGUAGES = 'settings.languages'; - -export const SEARCH_BOOK_PREFIX = 'sb_'; -export const SEARCH_AUTHORS_PREFIX = 'sa_'; -export const SEARCH_SERIES_PREFIX = 'ss_'; -export const SEARCH_TRANSLATORS_PREFIX = 'st_'; - -export const AUTHOR_BOOKS_PREFIX = 'ba_'; -export const TRANSLATOR_BOOKS_PREFIX = 'bt_'; -export const SEQUENCE_BOOKS_PREFIX = 'bs_'; - -export const BOOK_ANNOTATION_PREFIX = 'a_an_'; -export const AUTHOR_ANNOTATION_PREFIX = 'b_an_'; - -export const BOOK_INFO_PREFIX = 'b_i_'; - -export const RANDOM_BOOK = 'random_book'; -export const RANDOM_BOOK_BY_GENRE_REQUEST = 'random_book_by_genre_request'; -export const RANDOM_BOOK_BY_GENRE = 'random_book_by_genre_'; -export const RANDOM_AUTHOR = 'random_author'; -export const RANDOM_SEQUENCE = 'random_sequence'; - -export const GENRES_PREFIX = 'genres_'; - -export const LANG_SETTINGS = 'lang_settings'; -export const ENABLE_LANG_PREFIX = 'lang_on_'; -export const DISABLE_LANG_PREFIX = 'lang_off_'; - -export const UPDATE_LOG_PREFIX = 'update_log_'; - -export const RATE_PREFIX = 'r_'; diff --git a/src/bots/factory/bots/approved/errors_utils.ts b/src/bots/factory/bots/approved/errors_utils.ts deleted file mode 100644 index 165afd7..0000000 --- a/src/bots/factory/bots/approved/errors_utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { TelegramError } from 'telegraf'; - - -export function isNotModifiedMessage(e: any): boolean { - if (!(e instanceof TelegramError)) return false; - - return e.description === 'Bad Request: message is not modified: specified new message content and reply markup are exactly the same as a current content and reply markup of the message'; -} - -export function isReplyMessageNotFound(e: any): boolean { - if (!(e instanceof TelegramError)) return false; - - return e.description === 'Bad Request: replied message not found'; -} diff --git a/src/bots/factory/bots/approved/format.ts b/src/bots/factory/bots/approved/format.ts deleted file mode 100644 index 452135b..0000000 --- a/src/bots/factory/bots/approved/format.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { AuthorBook, TranslatorBook, Book, Author, Sequence, BookAuthor, DetailBook, Genre } from './services/book_library'; - - -type AllBookTypes = Book | AuthorBook | TranslatorBook; - - -function isAuthorBook(item: AllBookTypes): item is AuthorBook { - return 'translator' in item; -} - - -function isTranslatorBook(item: AllBookTypes): item is TranslatorBook { - return 'authors' in item; -} - - -export function formatBook(book: AllBookTypes, short: boolean = false): string { - let response: string[] = []; - - response.push(`📖 ${book.title} | ${book.lang}`); - - response.push(`Информация: /b_i_${book.id}`); - - const pushAuthorOrTranslator = (author: BookAuthor) => response.push( - `͏👤 ${author.last_name} ${author.first_name} ${author.middle_name}` - ); - - if (isTranslatorBook(book) && book.authors.length > 0) { - response.push('Авторы:') - - if (short && book.authors.length >= 5) { - book.authors.slice(0, 5).forEach(pushAuthorOrTranslator); - response.push(" и другие."); - } else { - book.authors.forEach(pushAuthorOrTranslator); - } - } - - if (isAuthorBook(book) && book.translators.length > 0) { - response.push('Переводчики:'); - - if (short && book.translators.length >= 5) { - book.translators.slice(0, 5).forEach(pushAuthorOrTranslator); - response.push(" и другие.") - } else { - book.translators.forEach(pushAuthorOrTranslator); - } - } - - book.available_types.forEach(a_type => response.push(`📥 ${a_type}: /d_${a_type}_${book.id}`)); - - return response.join('\n'); -} - -export function formatDetailBook(book: DetailBook): string { - let response: string[] = []; - - const addEmptyLine = () => response.push(""); - - response.push(`📖 ${book.title} | ${book.lang}`); - - if (book.pages !== null) - response.push(`[ ${book.pages} с. ]`); - - addEmptyLine(); - - if (book.annotation_exists) { - response.push(`📝 Аннотация: /b_an_${book.id}`) - addEmptyLine(); - } - - if (book.authors.length > 0) { - response.push('Авторы:') - - const pushAuthor = (author: BookAuthor) => response.push( - `͏👤 ${author.last_name} ${author.first_name} ${author.middle_name} /a_${author.id}` - ); - book.authors.forEach(pushAuthor); - addEmptyLine(); - } - - if (book.translators.length > 0) { - response.push('Переводчики:'); - - const pushTranslator = (author: BookAuthor) => response.push( - `͏👤 ${author.last_name} ${author.first_name} ${author.middle_name} /t_${author.id}` - ); - book.translators.forEach(pushTranslator); - addEmptyLine(); - } - - if (book.sequences.length > 0) { - response.push('Серии:'); - - const pushSequence = (sequence: Sequence) => response.push( - `📚 ${sequence.name} /s_${sequence.id}` - ); - book.sequences.forEach(pushSequence); - addEmptyLine(); - } - - if (book.genres.length > 0) { - response.push('Жанры:'); - - const pushGenre = (genre: Genre) => response.push( - `🗂 ${genre.description}` - ); - book.genres.forEach(pushGenre); - addEmptyLine(); - } - - response.push("Скачать: ") - book.available_types.forEach(a_type => response.push(`📥 ${a_type}: /d_${a_type}_${book.id}`)); - - return response.join('\n'); -} - - -export function formatDetailBookWithRating(book: DetailBook): string { - return formatDetailBook(book) + '\n\n\nОценка:'; -} - - -export function formatBookShort(book: AllBookTypes): string { - return formatBook(book, true); -} - - -export function formatAuthor(author: Author): string { - let response = []; - - response.push(`👤 ${author.last_name} ${author.first_name} ${author.middle_name}`); - response.push(`/a_${author.id}`); - - if (author.annotation_exists) { - response.push(`📝 Аннотация: /a_an_${author.id}`); - } - - return response.join('\n'); -} - - -export function formatTranslator(author: Author): string { - let response = []; - - response.push(`👤 ${author.last_name} ${author.first_name} ${author.middle_name}`); - response.push(`/t_${author.id}`); - - if (author.annotation_exists) { - response.push(`📝 Аннотация: /a_an_${author.id}`); - } - - return response.join('\n'); -} - - -export function formatSequence(sequence: Sequence): string { - let response = []; - - response.push(`📚 ${sequence.name}`); - response.push(`/s_${sequence.id}`); - - return response.join('\n'); -} diff --git a/src/bots/factory/bots/approved/hooks/downloading.ts b/src/bots/factory/bots/approved/hooks/downloading.ts deleted file mode 100644 index 49ef15d..0000000 --- a/src/bots/factory/bots/approved/hooks/downloading.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Context } from 'telegraf'; - -import { clearBookCache, getBookCache, downloadFromCache } from '../services/book_cache'; -import { getBookCacheBuffer } from '../services/book_cache_buffer'; -import { BotState, Cache } from '@/bots/manager/types'; - - -async function _sendFile(ctx: Context, state: BotState, chatId: number, id: number, format: string) { - const sendWithDownloadFromChannel = async () => { - const data = await downloadFromCache(id, format); - - if (data === null) { - await ctx.reply("Ошибка скачивания книги. Попробуйте позже"); - return - } - - await ctx.telegram.sendDocument(chatId, { source: data.source, filename: data.filename }, { caption: data.caption }); - } - - const getCachedMessage = async () => { - if (state.cache === Cache.ORIGINAL) { - return getBookCache(id, format); - } - - return getBookCacheBuffer(id, format); - }; - - const sendCached = async () => { - const cache = await getCachedMessage(); - await ctx.telegram.copyMessage(chatId, cache.chat_id, cache.message_id, { - allow_sending_without_reply: true, - }); - }; - - if (state.cache === Cache.NO_CACHE) { - return sendWithDownloadFromChannel(); - } - - try { - return await sendCached(); - } catch (e) { - await clearBookCache(id, format); - return sendCached(); - } -} - - -export async function sendFile(ctx: Context, state: BotState) { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const [_, format, id] = ctx.message.text.split('@')[0].split('_'); - const chatId = ctx.message.chat.id; - - const sendSendingAction = async () => { - await ctx.telegram.sendChatAction(chatId, "upload_document"); - } - const action = setInterval(() => sendSendingAction(), 5000); - - try { - sendSendingAction(); - return await _sendFile(ctx, state, chatId, parseInt(id), format); - } catch (e) { - await ctx.reply("Ошибка! Попробуйте позже :(", { - reply_to_message_id: ctx.message.message_id, - }); - throw e; - } finally { - clearInterval(action); - } -} diff --git a/src/bots/factory/bots/approved/hooks/setCommands.ts b/src/bots/factory/bots/approved/hooks/setCommands.ts deleted file mode 100644 index cca834d..0000000 --- a/src/bots/factory/bots/approved/hooks/setCommands.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Telegraf, TelegramError } from "telegraf"; - - -export async function setCommands(bot: Telegraf) { - async function setMyCommands() { - await bot.telegram.setMyCommands([ - {command: "random", description: "Попытать удачу"}, - {command: "update_log", description: "Обновления каталога"}, - {command: "settings", description: "Настройки"}, - {command: "support", description: "Поддержать разработчика"}, - // {command: "help", description: "Помощь"}, - ]); - } - - try { - await setMyCommands(); - } catch (e: unknown) { - if (e instanceof TelegramError && e.response.error_code === 429) { - setTimeout(() => setMyCommands(), 1000 * (e.response.parameters?.retry_after || 5)); - } - } -} \ No newline at end of file diff --git a/src/bots/factory/bots/approved/index.ts b/src/bots/factory/bots/approved/index.ts deleted file mode 100644 index f5bd3fd..0000000 --- a/src/bots/factory/bots/approved/index.ts +++ /dev/null @@ -1,476 +0,0 @@ -import { Context, Telegraf, Markup, TelegramError } from 'telegraf'; -import moment from 'moment'; -import debug from 'debug'; - -import { BotState } from '@/bots/manager/types'; - -import env from '@/config'; - -import * as Messages from "./messages"; - -import * as CallbackData from "./callback_data"; - -import * as BookLibrary from "./services/book_library"; -import * as Rating from "./services/book_ratings"; -import UsersCounter from '@/analytics/users_counter'; -import Limiter from '@/bots/limiter'; -import { createOrUpdateUserSettings, getUserOrDefaultLangCodes } from './services/user_settings'; -import { formatBook, formatBookShort, formatAuthor, formatSequence, formatTranslator, formatDetailBook, formatDetailBookWithRating } from './format'; -import { getCallbackArgs, getPaginatedMessage, getPrefixWithQueryCreator, getSearchArgs, registerLanguageSettingsCallback, registerPaginationCommand, registerRandomItemCallback } from './utils'; -import { getRandomKeyboard, getRatingKeyboard, getTextPaginationData, getUpdateLogKeyboard, getUserAllowedLangsKeyboard } from './keyboard'; -import { sendFile } from './hooks/downloading'; -import { setCommands } from './hooks/setCommands'; -import { isNotModifiedMessage, isReplyMessageNotFound } from './errors_utils'; -import { getAnnotationHandler } from './annotations'; -import Sentry from '@/sentry'; - - -const log = debug("approvedBot"); - - -export async function createApprovedBot(token: string, state: BotState): Promise { - const bot = new Telegraf(token, { - telegram: { - apiRoot: env.TELEGRAM_BOT_API_ROOT, - }, - handlerTimeout: 300_000, - }); - - const me = await bot.telegram.getMe(); - - setCommands(bot); - - bot.use(async (ctx: Context, next) => { - if (ctx.from) { - const user = ctx.from; - - createOrUpdateUserSettings({ - user_id: user.id, - last_name: user.last_name || '', - first_name: user.first_name, - username: user.username || '', - source: me.username, - }).catch((e) => { - Sentry.captureException(e); - }); - - UsersCounter.take(user.id, me.username); - } - await next(); - }); - - bot.use(async (ctx: Context, next) => { - if (await Limiter.isLimited(ctx.update.update_id)) return; - - await next(); - }); - - bot.command(["start", `start@${me.username}`], async (ctx: Context) => { - if (!ctx.message) { - return; - } - - const name = ctx.message.from.first_name || ctx.message.from.username || 'пользователь'; - - try { - await ctx.telegram.sendMessage(ctx.message.chat.id, - Messages.START_MESSAGE.replace('{name}', name), { - reply_to_message_id: ctx.message.message_id, - } - ); - } catch (e) { - if (e instanceof TelegramError) { - if (e.code !== 403) throw e; - } - } - }); - - bot.command(["help", `help@${me.username}`], async (ctx: Context) => ctx.reply(Messages.HELP_MESSAGE)); - - bot.command(["support", `support@{me.username}`], async (ctx: Context) => { - const keyboard = Markup.inlineKeyboard([ - Markup.button.url("☕️ Поддержать разработчика", "https://kurbezz.github.io/Kurbezz/") - ]); - - ctx.reply(Messages.SUPPORT_MESSAGE, {parse_mode: 'Markdown', reply_markup: keyboard.reply_markup}); - }); - - registerPaginationCommand( - bot, CallbackData.SEARCH_BOOK_PREFIX, getSearchArgs, null, BookLibrary.searchByBookName, formatBookShort, undefined, Messages.BOOKS_NOT_FOUND - ); - registerPaginationCommand( - bot, CallbackData.SEARCH_TRANSLATORS_PREFIX, getSearchArgs, null, BookLibrary.searchTranslators, formatTranslator, - undefined, Messages.TRANSLATORS_NOT_FOUND - ); - registerPaginationCommand( - bot, CallbackData.SEARCH_AUTHORS_PREFIX, getSearchArgs, null, BookLibrary.searchAuthors, formatAuthor, undefined, Messages.AUTHORS_NOT_FOUND - ); - registerPaginationCommand( - bot, CallbackData.SEARCH_SERIES_PREFIX, getSearchArgs, null, BookLibrary.searchSequences, formatSequence, undefined, Messages.SEQUENCES_NOT_FOUND - ); - - registerPaginationCommand( - bot, CallbackData.AUTHOR_BOOKS_PREFIX, getCallbackArgs, getPrefixWithQueryCreator(CallbackData.AUTHOR_BOOKS_PREFIX), - BookLibrary.getAuthorBooks, formatBookShort, undefined, Messages.BOOKS_NOT_FOUND, - ); - registerPaginationCommand( - bot, CallbackData.TRANSLATOR_BOOKS_PREFIX, getCallbackArgs, getPrefixWithQueryCreator(CallbackData.TRANSLATOR_BOOKS_PREFIX), - BookLibrary.getTranslatorBooks, formatBookShort, undefined, Messages.BOOKS_NOT_FOUND, - ); - registerPaginationCommand( - bot, CallbackData.SEQUENCE_BOOKS_PREFIX, getCallbackArgs, getPrefixWithQueryCreator(CallbackData.SEQUENCE_BOOKS_PREFIX), - BookLibrary.getSequenceBooks, formatBookShort, undefined, Messages.BOOKS_NOT_FOUND, - ); - - bot.command(["random", `random@${me.username}`], async (ctx: Context) => { - ctx.reply("Что хотим получить?", { - reply_markup: getRandomKeyboard().reply_markup, - }) - }); - - registerRandomItemCallback(bot, CallbackData.RANDOM_BOOK, BookLibrary.getRandomBook, formatDetailBook); - registerRandomItemCallback(bot, CallbackData.RANDOM_AUTHOR, BookLibrary.getRandomAuthor, formatAuthor); - registerRandomItemCallback(bot, CallbackData.RANDOM_SEQUENCE, BookLibrary.getRandomSequence, formatSequence); - - bot.action(CallbackData.RANDOM_BOOK_BY_GENRE_REQUEST, async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const metaGenres = await BookLibrary.getGenreMetas(); - - const keyboard = Markup.inlineKeyboard( - metaGenres.map((meta, index) => { - return [Markup.button.callback(meta, `${CallbackData.GENRES_PREFIX}${index}`)]; - }) - ); - - await ctx.editMessageReplyMarkup(keyboard.reply_markup); - }); - - bot.action(new RegExp(CallbackData.GENRES_PREFIX), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const queryData = ctx.callbackQuery.data.split("_"); - const metaIndex = parseInt(queryData[1]); - - const metaGenres = await BookLibrary.getGenreMetas(); - const meta = metaGenres[metaIndex]; - - const genres = await BookLibrary.getGenres(meta); - - const buttons = genres.items.map((genre) => { - return [Markup.button.callback(genre.description, `${CallbackData.RANDOM_BOOK_BY_GENRE}${genre.id}`)] - }); - buttons.push( - [Markup.button.callback("< Назад >", CallbackData.RANDOM_BOOK_BY_GENRE_REQUEST)] - ); - - const keyboard = Markup.inlineKeyboard(buttons); - - await ctx.editMessageReplyMarkup(keyboard.reply_markup); - }); - - bot.action(new RegExp(CallbackData.RANDOM_BOOK_BY_GENRE), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.callbackQuery.from.id); - - const queryData = ctx.callbackQuery.data.split("_"); - const genreId = parseInt(queryData[4]); - - const item = await BookLibrary.getRandomBook(allowedLangs, genreId); - const keyboard = Markup.inlineKeyboard([ - [Markup.button.callback("Повторить?", ctx.callbackQuery.data)] - ]); - - try { - await ctx.editMessageReplyMarkup(Markup.inlineKeyboard([]).reply_markup); - } catch (e) {} - - ctx.reply(formatDetailBook(item), { - reply_markup: keyboard.reply_markup, - }); - }); - - bot.command(["update_log", `update_log@${me.username}`], async (ctx: Context) => { - ctx.reply("Обновление каталога: ", { - reply_markup: getUpdateLogKeyboard().reply_markup, - }); - }); - - bot.action(new RegExp(CallbackData.UPDATE_LOG_PREFIX), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.callbackQuery.from.id); - - const data = ctx.callbackQuery.data.split("_"); - const page = parseInt(data[4]); - - const arg = `${data[2]}_${data[3]}`; - - const header = `Обновление каталога (${moment(data[2]).format("DD.MM.YYYY")} - ${moment(data[3]).format("DD.MM.YYYY")}):\n\n`; - const noItemsMessage = 'Нет новых книг за этот период.'; - - const pMessage = await getPaginatedMessage( - `${CallbackData.UPDATE_LOG_PREFIX}${arg}_`, arg, page, allowedLangs, BookLibrary.getBooks, formatBook, header, noItemsMessage, - ); - - try { - await ctx.editMessageText(pMessage.message, { - reply_markup: pMessage.keyboard?.reply_markup - }); - } catch (e) { - if (!isNotModifiedMessage(e)) { - Sentry.captureException(e); - } - } - }); - - bot.command(["settings", `settings@${me.username}`], async (ctx: Context) => { - const keyboard = Markup.inlineKeyboard([ - [Markup.button.callback("Языки", CallbackData.LANG_SETTINGS)] - ]); - - ctx.reply("Настройки:", { - reply_markup: keyboard.reply_markup - }); - }); - - bot.action(CallbackData.LANG_SETTINGS, async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const keyboard = await getUserAllowedLangsKeyboard(ctx.callbackQuery.from.id); - - try { - await ctx.editMessageText("Настройки языков:", { - reply_markup: keyboard.reply_markup, - }); - } catch (e) { - if (!isNotModifiedMessage(e)) { - Sentry.captureException(e); - } - } - }); - - registerLanguageSettingsCallback(bot, 'on', CallbackData.ENABLE_LANG_PREFIX); - registerLanguageSettingsCallback(bot, 'off', CallbackData.DISABLE_LANG_PREFIX); - - bot.hears(new RegExp(`^/d_[a-zA-Z0-9]+_[\\d]+(@${me.username})*$`), async (ctx) => { - try { - await sendFile(ctx, state) - } catch (e) { - Sentry.captureException(e, { - extra: { - action: "sendFile", - message: ctx.message.text, - } - }) - } - }); - - bot.hears( - new RegExp(`^/b_an_[\\d]+(@${me.username})*$`), - getAnnotationHandler(BookLibrary.getBookAnnotation, CallbackData.BOOK_ANNOTATION_PREFIX) - ); - - bot.action(new RegExp(CallbackData.BOOK_ANNOTATION_PREFIX), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const queryData = ctx.callbackQuery.data.split("_"); - - const bookId = queryData[2]; - const page = queryData[3]; - - const annotation = await BookLibrary.getBookAnnotation(parseInt(bookId)); - - const data = getTextPaginationData(`${CallbackData.BOOK_ANNOTATION_PREFIX}${bookId}`, annotation.text, parseInt(page)); - - try { - await ctx.editMessageText( - data.current, { - parse_mode: "HTML", - reply_markup: data.keyboard.reply_markup, - } - ); - } catch (e) { - if (!isNotModifiedMessage(e)) { - Sentry.captureException(e, { - extra: { - message: data.current, - bookId, - page, - } - }); - } - } - }); - - bot.hears( - new RegExp(`^/a_an_[\\d]+(@${me.username})*$`), - getAnnotationHandler(BookLibrary.getAuthorAnnotation, CallbackData.AUTHOR_ANNOTATION_PREFIX) - ); - - bot.action(new RegExp(CallbackData.AUTHOR_ANNOTATION_PREFIX), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const queryData = ctx.callbackQuery.data.split("_"); - - const authorId = queryData[2]; - const page = queryData[3]; - - const annotation = await BookLibrary.getAuthorAnnotation(parseInt(authorId)); - - const data = getTextPaginationData(`${CallbackData.AUTHOR_ANNOTATION_PREFIX}${authorId}`, annotation.text, parseInt(page)); - - try { - await ctx.editMessageText( - data.current, { - parse_mode: "HTML", - reply_markup: data.keyboard.reply_markup, - } - ); - } catch (e) { - if (!isNotModifiedMessage(e)) { - Sentry.captureException(e, { - extra: { - message: data.current, - authorId, - page, - } - }); - } - } - }); - - bot.hears(new RegExp(`^/a_[\\d]+(@${me.username})*$`), async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const authorId = ctx.message.text.split('@')[0].split('_')[1]; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.message.from.id); - - const pMessage = await getPaginatedMessage( - `${CallbackData.AUTHOR_BOOKS_PREFIX}${authorId}_`, parseInt(authorId), 1, - allowedLangs, BookLibrary.getAuthorBooks, formatBook, undefined, Messages.BOOKS_NOT_FOUND - ); - - await ctx.reply(pMessage.message, { - reply_markup: pMessage.keyboard?.reply_markup - }); - }); - - bot.hears(new RegExp(`^/t_[\\d]+(@${me.username})*$`), async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const translatorId = ctx.message.text.split("@")[0].split('_')[1]; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.message.from.id); - - const pMessage = await getPaginatedMessage( - `${CallbackData.TRANSLATOR_BOOKS_PREFIX}${translatorId}_`, parseInt(translatorId), 1, - allowedLangs, BookLibrary.getTranslatorBooks, formatBook, undefined, Messages.BOOKS_NOT_FOUND - ); - - await ctx.reply(pMessage.message, { - reply_markup: pMessage.keyboard?.reply_markup - }); - }); - - bot.hears(new RegExp(`^/s_[\\d]+(@${me.username})*$`), async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const sequenceId = ctx.message.text.split("@")[0].split('_')[1]; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.message.from.id); - - const pMessage = await getPaginatedMessage( - `${CallbackData.SEQUENCE_BOOKS_PREFIX}${sequenceId}_`, parseInt(sequenceId), 1, allowedLangs, - BookLibrary.getSequenceBooks, formatBook, undefined, Messages.BOOKS_NOT_FOUND, - ); - - await ctx.reply(pMessage.message, { - reply_markup: pMessage.keyboard?.reply_markup - }); - }); - - bot.hears(new RegExp(`^/b_i_[\\d]+(@${me.username})*$`), async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - const bookIdString = ctx.message.text.split("@")[0].split('_')[2]; - const bookId = parseInt(bookIdString); - - const book = await BookLibrary.getBookById(bookId); - const keyboard = await getRatingKeyboard(ctx.message.from.id, bookId, null); - - await ctx.reply(formatDetailBookWithRating(book), { - reply_to_message_id: ctx.message.message_id, - reply_markup: keyboard.reply_markup, - }); - }); - - bot.action(new RegExp(CallbackData.RATE_PREFIX), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const queryData = ctx.callbackQuery.data.split("_"); - - const userId = parseInt(queryData[1]); - const bookId = parseInt(queryData[2]); - const rate = parseInt(queryData[3]); - - const rating = await Rating.set(userId, bookId, rate); - - const keyboard = await getRatingKeyboard(userId, bookId, rating); - - try { - await ctx.editMessageReplyMarkup( - keyboard.reply_markup - ); - } catch (e) {} - }); - - bot.on("message", async (ctx: Context) => { - if (!ctx.message || !('text' in ctx.message)) { - return; - } - - let keyboard = Markup.inlineKeyboard([ - [ - Markup.button.callback('Книгу', `${CallbackData.SEARCH_BOOK_PREFIX}1`) - ], - [ - Markup.button.callback('Автора', `${CallbackData.SEARCH_AUTHORS_PREFIX}1`), - ], - [ - Markup.button.callback('Серию', `${CallbackData.SEARCH_SERIES_PREFIX}1`), - ], - [ - Markup.button.callback('Переводчика', `${CallbackData.SEARCH_TRANSLATORS_PREFIX}1`), - ] - ]); - - try { - await ctx.telegram.sendMessage(ctx.message.chat.id, Messages.SEARCH_MESSAGE, { - reply_to_message_id: ctx.message.message_id, - reply_markup: keyboard.reply_markup, - }); - } catch (e) { - if (!isReplyMessageNotFound(e)) { - Sentry.captureException(e); - } - } - }); - - bot.catch((err, ctx: Context) => { - log({err, ctx}); - Sentry.captureException(err); - }); - - return bot; -} diff --git a/src/bots/factory/bots/approved/keyboard.ts b/src/bots/factory/bots/approved/keyboard.ts deleted file mode 100644 index ef15170..0000000 --- a/src/bots/factory/bots/approved/keyboard.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Markup } from 'telegraf'; -import { InlineKeyboardMarkup } from 'typegram'; -import moment from 'moment'; -import chunkText from 'chunk-text'; - -import { RANDOM_BOOK, RANDOM_AUTHOR, RANDOM_SEQUENCE, ENABLE_LANG_PREFIX, DISABLE_LANG_PREFIX, UPDATE_LOG_PREFIX, RATE_PREFIX, RANDOM_BOOK_BY_GENRE, RANDOM_BOOK_BY_GENRE_REQUEST } from './callback_data'; -import { getLanguages, getUserOrDefaultLangCodes } from './services/user_settings'; -import * as BookRating from "./services/book_ratings"; - - -function getButtonLabel(delta: number, direction: 'left' | 'right'): string { - if (delta == 1) { - return direction === 'left' ? "<" : ">"; - } - - return direction === 'left' ? `< ${delta} <` : `> ${delta} >`; -} - - -export function getPaginationKeyboard(prefix: string, query: string | number, page: number, totalPages: number): Markup.Markup { - function getRow(delta: number) { - const row = []; - - if (page - delta > 0) { - row.push(Markup.button.callback(getButtonLabel(delta, 'left'), `${prefix}${page - delta}`)); - } - if (page + delta <= totalPages) { - row.push(Markup.button.callback(getButtonLabel(delta, 'right'), `${prefix}${page + delta}`)); - } - - return row; - } - - const rows = []; - - const row1 = getRow(1); - if (row1) { - rows.push(row1); - } - - const row5 = getRow(5); - if (row5) { - rows.push(row5); - } - - return Markup.inlineKeyboard(rows); -} - - -export function getTextPaginationData(prefix: string, text: string, currentPage: number): {current: string, keyboard: Markup.Markup} { - const chunks = chunkText(text, 512).filter((chunk) => chunk.length !== 0); - - const current = chunks[currentPage]; - - const row = []; - - if (currentPage - 1 >= 0) { - row.push(Markup.button.callback("<", `${prefix}_${currentPage - 1}`)); - } - - if (currentPage + 1 < chunks.length) { - row.push(Markup.button.callback(">", `${prefix}_${currentPage + 1}`)); - } - - const keyboard = Markup.inlineKeyboard([row]); - - return { - current, - keyboard, - } -} - - -export function getRandomKeyboard(): Markup.Markup { - return Markup.inlineKeyboard([ - [Markup.button.callback('Книгу', RANDOM_BOOK)], - [Markup.button.callback('Книгу по жанру', RANDOM_BOOK_BY_GENRE_REQUEST)], - [Markup.button.callback('Автора', RANDOM_AUTHOR)], - [Markup.button.callback('Серию', RANDOM_SEQUENCE)], - ]); -} - - -export function getUpdateLogKeyboard(): Markup.Markup { - const format = "YYYY-MM-DD"; - - const now = moment().format(format); - const d3 = moment().subtract(3, 'days').format(format); - const d7 = moment().subtract(7, 'days').format(format); - const d30 = moment().subtract(30, 'days').format(format); - - return Markup.inlineKeyboard([ - [Markup.button.callback('За 3 дня', `${UPDATE_LOG_PREFIX}${d3}_${now}_1`)], - [Markup.button.callback('За 7 дней', `${UPDATE_LOG_PREFIX}${d7}_${now}_1`)], - [Markup.button.callback('За 30 дней', `${UPDATE_LOG_PREFIX}${d30}_${now}_1`)], - ]); -} - -export async function getUserAllowedLangsKeyboard(userId: number): Promise> { - const allLangs = await getLanguages(); - const userAllowedLangsCodes = await getUserOrDefaultLangCodes(userId); - - const onEmoji = '🟢'; - const offEmoji = '🔴'; - - return Markup.inlineKeyboard([ - ...allLangs.map((lang) => { - let titlePrefix: string; - let callbackDataPrefix: string; - if (userAllowedLangsCodes.includes(lang.code)) { - titlePrefix = onEmoji; - callbackDataPrefix = DISABLE_LANG_PREFIX; - } else { - titlePrefix = offEmoji; - callbackDataPrefix = ENABLE_LANG_PREFIX; - } - const title = `${titlePrefix} ${lang.label}`; - return [Markup.button.callback(title, `${callbackDataPrefix}${lang.code}`)]; - }) - ]); -} - -export async function getRatingKeyboard(userId: number, bookId: number, rating: BookRating.Rating | null): Promise> { - const bookRating = rating ? rating : await BookRating.get(userId, bookId); - - const rate = bookRating ? bookRating.rate : null; - - return Markup.inlineKeyboard([ - [1, 2, 3, 4, 5].map((bRate) => { - const title = bRate === rate ? `⭐️ ${bRate}` : bRate.toString(); - - return Markup.button.callback(title, `${RATE_PREFIX}${userId}_${bookId}_${bRate}`); - }) - ]); -} diff --git a/src/bots/factory/bots/approved/messages.ts b/src/bots/factory/bots/approved/messages.ts deleted file mode 100644 index 4f9f932..0000000 --- a/src/bots/factory/bots/approved/messages.ts +++ /dev/null @@ -1,22 +0,0 @@ -export const START_MESSAGE = 'Привет, {name}! \n' + - 'Этот бот поможет тебе загружать книги.\n' + - // 'Узнать, как со мной работать /help.\n' + - 'Настройки языков для поиска /settings.\n'; - -export const HELP_MESSAGE = 'Пока пусто :('; - -export const SETTINGS_MESSAGE = 'Настройки:'; - -export const SEARCH_MESSAGE = 'Что ищем?'; - -export const BOOKS_NOT_FOUND = "Книги не найдены."; - -export const AUTHORS_NOT_FOUND = "Авторы не найдены."; - -export const TRANSLATORS_NOT_FOUND = "Переводчики не найдены."; - -export const SEQUENCES_NOT_FOUND = "Серии не найдены."; - -export const SUPPORT_MESSAGE = - '[Лицензии](https://github.com/flibusta-apps/book_bot/blob/main/LICENSE.md) \n\n' + - '[Исходный код](https://github.com/flibusta-apps)\n'; \ No newline at end of file diff --git a/src/bots/factory/bots/approved/services/book_cache.ts b/src/bots/factory/bots/approved/services/book_cache.ts deleted file mode 100644 index 773f4a7..0000000 --- a/src/bots/factory/bots/approved/services/book_cache.ts +++ /dev/null @@ -1,99 +0,0 @@ -import got, { Response } from 'got'; -import { decode } from 'js-base64'; - -import env from '@/config'; - - -export interface CachedMessage { - message_id: number, - chat_id: string | number, -} - - -interface BookCache { - id: number; - object_id: number; - object_type: string; - data: CachedMessage & { - file_token: string | null, - } -} - - -async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { - const response = await got(`${env.CACHE_SERVER_URL}${url}`, { - searchParams, - headers: { - 'Authorization': env.CACHE_SERVER_API_KEY, - }, - responseType: 'json', - }); - - return response.body; -} - -async function _makeDeleteRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { - const response = await got.delete(`${env.CACHE_SERVER_URL}${url}`, { - searchParams, - headers: { - 'Authorization': env.CACHE_SERVER_API_KEY, - }, - responseType: 'json', - }); - - return response.body; -} - - -export async function getBookCache(bookId: number, fileType: string): Promise { - return (await _makeRequest(`/api/v1/${bookId}/${fileType}`)).data; -} - -export async function clearBookCache(bookId: number, fileType: string): Promise { - return (await _makeDeleteRequest(`/api/v1/${bookId}/${fileType}`)).data; -} - -export interface DownloadedFile { - source: NodeJS.ReadableStream; - filename: string; - caption: string; -} - -export async function downloadFromCache(bookId: number, fileType: string): Promise { - const readStream = got.stream.get(`${env.CACHE_SERVER_URL}/api/v1/download/${bookId}/${fileType}`, { - headers: { - 'Authorization': env.CACHE_SERVER_API_KEY, - }, - }); - - return new Promise((resolve, reject) => { - let timeout: NodeJS.Timeout | null = null; - - const resolver = async (response: Response) => { - if (response.statusCode !== 200) { - resolve(null); - if (timeout) clearTimeout(timeout); - return - } - - const captionData = response.headers['x-caption-b64']; - - if (captionData === undefined || Array.isArray(captionData)) throw Error('No caption?'); - - if (timeout) clearTimeout(timeout); - - return resolve({ - source: readStream, - filename: (response.headers['content-disposition'] || '').replaceAll('"', "").split('filename=')[1], - caption: decode(captionData), - }) - } - - timeout = setTimeout(() => { - readStream.off("response", resolver); - resolve(null); - }, 60_000); - - readStream.on("response", resolver); - }); -} diff --git a/src/bots/factory/bots/approved/services/book_cache_buffer.ts b/src/bots/factory/bots/approved/services/book_cache_buffer.ts deleted file mode 100644 index 212dac9..0000000 --- a/src/bots/factory/bots/approved/services/book_cache_buffer.ts +++ /dev/null @@ -1,22 +0,0 @@ -import got from 'got'; - -import env from '@/config'; -import { CachedMessage } from './book_cache'; - - -async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { - const response = await got(`${env.BUFFER_SERVER_URL}${url}`, { - searchParams, - headers: { - 'Authorization': env.BUFFER_SERVER_API_KEY, - }, - responseType: 'json', - }); - - return response.body; -} - - -export async function getBookCacheBuffer(bookId: number, fileType: string): Promise { - return _makeRequest(`/api/v1/${bookId}/${fileType}`); -} diff --git a/src/bots/factory/bots/approved/services/book_library.ts b/src/bots/factory/bots/approved/services/book_library.ts deleted file mode 100644 index e6664a8..0000000 --- a/src/bots/factory/bots/approved/services/book_library.ts +++ /dev/null @@ -1,235 +0,0 @@ -import got from 'got'; - -import env from '@/config'; -import { getAllowedLangsSearchParams } from '../utils'; - - -const PAGE_SIZE = 7; - - -export interface Page { - items: T[]; - page: number; - size: number; - total: number; - total_pages: number; -} - - -export interface BookAuthor { - id: number; - first_name: string; - last_name: string; - middle_name: string; -} - - -export interface BaseBook { - id: number; - title: string; - lang: string; - file_type: string; - available_types: string[]; - uploaded: string; - annotation_exists: boolean; -} - - -export interface AuthorBook extends BaseBook { - translators: BookAuthor[]; -} - - -export interface TranslatorBook extends BaseBook { - authors: BookAuthor[]; -} - - -export interface Book extends BaseBook { - authors: BookAuthor[]; - translators: BookAuthor[]; -} - - -export interface Genre { - id: number; - description: string; -} - - -export interface Source { - id: number; - name: string; -} - - -export interface DetailBook extends Book { - sequences: Sequence[]; - genres: Genre[]; - source: Source; - remote_id: number; - is_deleted: boolean; - pages: number | null; -} - - -export interface Author { - id: number; - last_name: string; - first_name: string; - middle_name: string; - annotation_exists: boolean; -} - - -export interface Sequence { - id: number; - name: string; -} - - -export interface AuthorAnnotation { - id: number; - title: string; - text: string; - file: string | null; -} - - -export interface BookAnnotation { - id: number; - title: string; - text: string; - file: string | null; -} - - -async function _makeRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { - const response = await got(`${env.BOOK_SERVER_URL}${url}`, { - searchParams, - headers: { - 'Authorization': env.BOOK_SERVER_API_KEY, - }, - responseType: 'json', - }); - - return response.body; -} - - -export async function getBooks(query: string, page: number, allowedLangs: string[]): Promise> { - const queryDates = query.split("_"); - - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - searchParams.append('uploaded_gte', queryDates[0]); - searchParams.append('uploaded_lte', queryDates[1]); - searchParams.append('is_deleted', 'false'); - - return _makeRequest>(`/api/v1/books/`, searchParams); -} - - -export async function getBookById(book_id: number): Promise { - return _makeRequest(`/api/v1/books/${book_id}`); -} - - -export async function searchByBookName(query: string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/books/search/${query}`, searchParams); -} - - -export async function searchAuthors(query: string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/authors/search/${query}`, searchParams); -} - - -export async function searchTranslators(query: string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/translators/search/${query}`, searchParams); -} - - -export async function searchSequences(query: string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/sequences/search/${query}`, searchParams); -} - - -export async function getBookAnnotation(bookId: number): Promise { - return _makeRequest(`/api/v1/books/${bookId}/annotation`); -} - - -export async function getAuthorAnnotation(authorId: number): Promise { - return _makeRequest(`/api/v1/authors/${authorId}/annotation`); -} - - -export async function getAuthorBooks(authorId: number | string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/authors/${authorId}/books`, searchParams); -} - - -export async function getTranslatorBooks(translatorId: number | string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/translators/${translatorId}/books`, searchParams); -} - - -export async function getSequenceBooks(sequenceId: number | string, page: number, allowedLangs: string[]): Promise> { - const searchParams = getAllowedLangsSearchParams(allowedLangs); - searchParams.append('page', page.toString()); - searchParams.append('size', PAGE_SIZE.toString()); - - return _makeRequest>(`/api/v1/sequences/${sequenceId}/books`, searchParams); -} - -export async function getRandomBook(allowedLangs: string[], genre: number | null = null): Promise { - const params = getAllowedLangsSearchParams(allowedLangs); - if (genre) params.append("genre", genre.toString()); - - return _makeRequest( - '/api/v1/books/random', - params, - ); -} - -export async function getRandomAuthor(allowedLangs: string[]): Promise { - return _makeRequest('/api/v1/authors/random', getAllowedLangsSearchParams(allowedLangs)); -} - -export async function getRandomSequence(allowedLangs: string[]): Promise { - return _makeRequest('/api/v1/sequences/random', getAllowedLangsSearchParams(allowedLangs)); -} - -export async function getGenreMetas(): Promise { - return _makeRequest('/api/v1/genres/metas'); -} - -export async function getGenres(meta: string): Promise> { - return _makeRequest>('/api/v1/genres', {meta}); -} diff --git a/src/bots/factory/bots/approved/services/book_ratings.ts b/src/bots/factory/bots/approved/services/book_ratings.ts deleted file mode 100644 index ce00f93..0000000 --- a/src/bots/factory/bots/approved/services/book_ratings.ts +++ /dev/null @@ -1,46 +0,0 @@ -import got from 'got'; - -import env from '@/config'; - - -export interface Rating { - id: number; - user_id: number; - book_id: number; - rate: number; - updated: string; -} - - -export async function get(userId: number, bookId: number): Promise { - try { - const response = await got(`${env.RATINGS_URL}/api/v1/ratings/${userId}/${bookId}`, { - headers: { - 'Authorization': env.RATINGS_API_KEY, - }, - responseType: 'json', - }); - - return response.body; - } catch { - return null; - } -} - - -export async function set(userId: number, bookId: number, rate: number): Promise { - const response = await got.post(`${env.RATINGS_URL}/api/v1/ratings`, { - json: { - "user_id": userId, - "book_id": bookId, - "rate": rate, - }, - headers: { - 'Authorization': env.RATINGS_API_KEY, - 'Content-Type': 'application/json', - }, - responseType: 'json' - }); - - return response.body; -} diff --git a/src/bots/factory/bots/approved/services/downloader.ts b/src/bots/factory/bots/approved/services/downloader.ts deleted file mode 100644 index 5bbc4d1..0000000 --- a/src/bots/factory/bots/approved/services/downloader.ts +++ /dev/null @@ -1,46 +0,0 @@ -import got from 'got'; - -import env from '@/config'; - - -export interface DownloadedFile { - source: NodeJS.ReadableStream; - filename: string; -} - - -export async function download(source_id: number, remote_id: number, file_type: string): Promise { - const readStream = got.stream.get(`${env.DOWNLOADER_URL}/download/${source_id}/${remote_id}/${file_type}`, { - headers: { - 'Authorization': env.DOWNLOADER_API_KEY, - }, - }); - - return new Promise((resolve, reject) => { - readStream.on("response", async response => { - resolve({ - source: readStream, - filename: (response.headers['content-disposition'] || '').split('filename=')[1] - }); - }); - }); -} - - -export async function downloadImage(path: string): Promise { - const readStream = got.stream.get(path, {throwHttpErrors: false}); - - return new Promise((resolve, reject) => { - readStream.on("response", async response => { - if (response.statusCode === 200) { - resolve(readStream); - } else { - resolve(null); - } - }); - - readStream.once("error", error => { - resolve(null); - }) - }); -} diff --git a/src/bots/factory/bots/approved/services/user_settings.ts b/src/bots/factory/bots/approved/services/user_settings.ts deleted file mode 100644 index 9cb5c40..0000000 --- a/src/bots/factory/bots/approved/services/user_settings.ts +++ /dev/null @@ -1,75 +0,0 @@ -import got from 'got'; - -import env from '@/config'; - - -interface Language { - id: number; - label: string; - code: string; -} - - -interface UserSettings { - user_id: number; - last_name: string; - first_name: string; - source: string; - allowed_langs: Language[]; -} - - -export interface UserSettingsUpdateData { - user_id: number; - last_name: string; - first_name: string; - username: string; - source: string; - allowed_langs?: string[]; -} - - -async function _makeGetRequest(url: string, searchParams?: string | Record | URLSearchParams | undefined): Promise { - const response = await got(`${env.USER_SETTINGS_URL}${url}`, { - searchParams, - headers: { - 'Authorization': env.USER_SETTINGS_API_KEY, - }, - responseType: 'json' - }); - - return response.body; -} - - -export async function getLanguages(): Promise { - return _makeGetRequest('/languages'); -} - - -export async function getUserSettings(userId: number): Promise { - return _makeGetRequest(`/users/${userId}`); -} - - -export async function getUserOrDefaultLangCodes(userId: number): Promise { - try { - return (await getUserSettings(userId)).allowed_langs.map((lang) => lang.code); - } catch { - return ["ru", "be", "uk"]; - } -} - - -export async function createOrUpdateUserSettings(data: UserSettingsUpdateData): Promise { - const response = await got.post(`${env.USER_SETTINGS_URL}/users/`, { - json: data, - headers: { - 'Authorization': env.USER_SETTINGS_API_KEY, - 'Content-Type': 'application/json', - }, - responseType: 'json' - }); - - return response.body; -} diff --git a/src/bots/factory/bots/approved/utils.ts b/src/bots/factory/bots/approved/utils.ts deleted file mode 100644 index 81dd797..0000000 --- a/src/bots/factory/bots/approved/utils.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { Context, Markup, Telegraf } from 'telegraf'; -import { InlineKeyboardMarkup } from 'typegram'; -import { URLSearchParams } from 'url'; - -import { isNotModifiedMessage } from './errors_utils'; -import { getPaginationKeyboard, getUserAllowedLangsKeyboard } from './keyboard'; -import * as BookLibrary from "./services/book_library"; -import { createOrUpdateUserSettings, getUserOrDefaultLangCodes } from './services/user_settings'; -import Sentry from '@/sentry'; - - -interface PreparedMessage { - message: string; - keyboard?: Markup.Markup; -} - - -export async function getPaginatedMessage( - prefix: string, - data: D, - page: number, - allowedLangs: string[], - itemsGetter: (data: D, page: number, allowedLangs: string[]) => Promise>, - itemFormater: (item: T) => string, - header: string = "", - noItemsMessage: string = "", -): Promise { - const itemsPage = await itemsGetter(data, page, allowedLangs); - - if (itemsPage.total_pages === 0) { - return { - message: noItemsMessage, - } - } - - if (page > itemsPage.total_pages) { - return getPaginatedMessage(prefix, data, itemsPage.total_pages, allowedLangs, itemsGetter, itemFormater, header, noItemsMessage); - } - - const formatedItems = itemsPage.items.map(itemFormater).join('\n\n\n'); - const message = header + formatedItems + `\n\nСтраница ${page}/${itemsPage.total_pages}`; - - const keyboard = getPaginationKeyboard(prefix, data, page, itemsPage.total_pages); - - return { - message, - keyboard - }; -} - - -export function registerPaginationCommand( - bot: Telegraf, - prefix: string, - argsGetter: (ctx: Context) => { query: Q, page: number } | null, - prefixCreator: ((query: Q) => string) | null, - itemsGetter: (data: Q, page: number, allowedLangs: string[]) => Promise>, - itemFormater: (item: T) => string, - headers?: string, - noItemsMessage?: string, -) { - bot.action(new RegExp(prefix), async (ctx: Context) => { - if (!ctx.callbackQuery) return; - - const args = argsGetter(ctx); - - if (args === null) return; - - const { query, page } = args; - - const allowedLangs = await getUserOrDefaultLangCodes(ctx.callbackQuery.from.id); - - const tPrefix = prefixCreator ? prefixCreator(query) : prefix; - - const pMessage = await getPaginatedMessage( - tPrefix, query, page, allowedLangs, itemsGetter, itemFormater, headers, noItemsMessage, - ); - - try { - await ctx.editMessageText(pMessage.message, { - reply_markup: pMessage.keyboard?.reply_markup - }); - } catch (e) { - if (!isNotModifiedMessage(e)) { - Sentry.captureException(e); - } - } - }) -} - -export function registerRandomItemCallback( - bot: Telegraf, - callback_data: string, - itemGetter: (allowedLangs: string[]) => Promise, - itemFormatter: (item: T) => string, -) { - bot.action(callback_data, async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - const item = await itemGetter( - await getUserOrDefaultLangCodes(ctx.callbackQuery.from.id), - ); - - const keyboard = Markup.inlineKeyboard([ - [Markup.button.callback("Повторить?", callback_data)] - ]); - - try { - await ctx.editMessageReplyMarkup(Markup.inlineKeyboard([]).reply_markup); - } catch (e) {} - - ctx.reply(itemFormatter(item), { - reply_markup: keyboard.reply_markup, - }); - }); -} - - -export function registerLanguageSettingsCallback( - bot: Telegraf, - action: 'on' | 'off', - prefix: string, -) { - bot.action(new RegExp(prefix), async (ctx: Context) => { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) return; - - let allowedLangsCodes = await getUserOrDefaultLangCodes(ctx.callbackQuery.from.id); - - const tLang = ctx.callbackQuery.data.split("_")[2]; - - if (action === 'on') { - allowedLangsCodes.push(tLang); - } else { - allowedLangsCodes = allowedLangsCodes.filter((item) => item !== tLang); - } - - if (allowedLangsCodes.length === 0) { - ctx.answerCbQuery("Должен быть активен, хотя бы один язык!", { - show_alert: true, - }); - return; - } - - const user = ctx.callbackQuery.from; - await createOrUpdateUserSettings({ - user_id: user.id, - last_name: user.last_name || '', - first_name: user.first_name, - username: user.username || '', - source: ctx.botInfo.username, - allowed_langs: allowedLangsCodes, - }); - - const keyboard = await getUserAllowedLangsKeyboard(user.id); - - try { - await ctx.editMessageReplyMarkup(keyboard.reply_markup); - } catch {} - }); -} - -export function getAllowedLangsSearchParams(allowedLangs: string[]): URLSearchParams { - const sp = new URLSearchParams(); - allowedLangs.forEach((lang) => sp.append('allowed_langs', lang)); - return sp; -} - - -const fail = (ctx: Context) => ctx.reply("Ошибка! Повторите поиск :("); - - -export function getSearchArgs(ctx: Context): { query: string, page: number } | null { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) { - fail(ctx) - return null; - } - if (!ctx.callbackQuery.message || !('reply_to_message' in ctx.callbackQuery.message)) { - fail(ctx); - return null; - } - - if (!ctx.callbackQuery.message.reply_to_message || !('text' in ctx.callbackQuery.message.reply_to_message)) { - fail(ctx) - return null; - } - - const page = parseInt(ctx.callbackQuery.data.split('_')[1]); - - if (isNaN(page)) { - fail(ctx) - return null; - } - - const query = ctx.callbackQuery.message.reply_to_message.text - .replaceAll("/", ""); - - return { query, page }; -} - -export function getCallbackArgs(ctx: Context): { query: string, page: number} | null { - if (!ctx.callbackQuery || !('data' in ctx.callbackQuery)) { - fail(ctx) - return null; - } - - const [ _, query, sPage ] = ctx.callbackQuery.data.split('_'); - - const page = parseInt(sPage); - - if (isNaN(page)) { - fail(ctx) - return null; - } - - return { query, page }; -} - -export function getPrefixWithQueryCreator(prefix: string) { - return (query: string) => `${prefix}${query}_`; -} - -export function isNormalText(value: string | null): boolean { - if (value === null) return false; - if (value.length === 0) return false; - if (value.replaceAll("\n", "").replaceAll(" ", "").length === 0) return false; - - return true; -} diff --git a/src/bots/factory/bots/blocked.ts b/src/bots/factory/bots/blocked.ts deleted file mode 100644 index 01a15ef..0000000 --- a/src/bots/factory/bots/blocked.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Telegraf, Context } from 'telegraf'; - -import { BotState } from '@/bots/manager/types'; - -import env from '@/config'; - - -export async function createBlockedBot(token: string, state: BotState): Promise { - const bot = new Telegraf(token, { - telegram: { - apiRoot: env.TELEGRAM_BOT_API_ROOT, - } - }); - - await bot.telegram.deleteMyCommands(); - - bot.on("message", async (ctx: Context) => { - await ctx.reply('Бот заблокирован!'); - }); - - return bot; -} diff --git a/src/bots/factory/bots/pending.ts b/src/bots/factory/bots/pending.ts deleted file mode 100644 index c8d2331..0000000 --- a/src/bots/factory/bots/pending.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Telegraf, Context } from 'telegraf'; - -import { BotState } from '@/bots/manager/types'; - -import env from '@/config'; - - -export async function createPendingBot(token: string, state: BotState): Promise { - const bot = new Telegraf(token, { - telegram: { - apiRoot: env.TELEGRAM_BOT_API_ROOT, - } - }); - - await bot.telegram.deleteMyCommands(); - - bot.on("message", async (ctx: Context) => { - await ctx.reply( - 'Бот зарегистрирован, но не подтвержден администратором! \n' + - 'Подтверждение занимает примерно 12 часов.' - ); - }); - - return bot; -} diff --git a/src/bots/factory/index.ts b/src/bots/factory/index.ts deleted file mode 100644 index 8d9f1a3..0000000 --- a/src/bots/factory/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Telegraf } from "telegraf"; - -import { BotState } from '@/bots/manager/types'; - -import { createPendingBot } from './bots/pending'; -import { createBlockedBot } from './bots/blocked'; -import { createApprovedBot } from './bots/approved/index'; - - -export enum BotStatuses { - PENDING = 'pending', - APPROVED = 'approved', - BLOCKED = 'blocked', -} - - -export default async function getBot(token: string, state: BotState): Promise { - const handlers = { - [BotStatuses.PENDING]: createPendingBot, - [BotStatuses.BLOCKED]: createBlockedBot, - [BotStatuses.APPROVED]: createApprovedBot, - }; - - return handlers[state.status](token, state); -} diff --git a/src/bots/limiter/index.ts b/src/bots/limiter/index.ts deleted file mode 100644 index f96a498..0000000 --- a/src/bots/limiter/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { createClient, RedisClientType } from 'redis'; - -import env from '@/config'; - -import debug from 'debug'; -import Sentry from '@/sentry'; - - -export default class Limiter { - static debugger = debug("limiter"); - static MAX_PROCESSING_COUNT: number = 3; - static _redisClient: RedisClientType | null = null; - - static async _getClient() { - if (this._redisClient === null) { - this._redisClient = createClient({ - url: `redis://${env.REDIS_HOST}:${env.REDIS_PORT}/${env.REDIS_DB}` - }); - - this._redisClient.on('error', (err) => { - console.log(err); - Sentry.captureException(err); - }); - - await this._redisClient.connect(); - } - - return this._redisClient; - } - - static _getKey(updateId: number) { - return `update_${updateId}`; - } - - static async _getCount(updateId: number): Promise { - const key = this._getKey(updateId); - - const client = await this._getClient(); - - await client.set(key, 0, {EX: 5 * 60, NX: true}); - return client.incr(key); - } - - static async isLimited(updateId: number): Promise { - const count = await this._getCount(updateId); - - this.debugger(`${updateId}: ${count}`); - - return count > this.MAX_PROCESSING_COUNT; - } -} diff --git a/src/bots/manager/index.ts b/src/bots/manager/index.ts deleted file mode 100644 index 68093ee..0000000 --- a/src/bots/manager/index.ts +++ /dev/null @@ -1,175 +0,0 @@ -import express, { Response, Request, NextFunction } from 'express'; -import { Server } from 'http'; - -import * as dockerIpTools from "docker-ip-get"; - -import { Telegraf } from 'telegraf'; - -import env from '@/config'; -import getBot from '../factory/index'; -import UsersCounter from '@/analytics/users_counter'; -import { makeSyncRequest } from "./utils"; -import { BotState } from "./types"; -import Sentry from '@/sentry'; - - -export default class BotsManager { - static server: Server | null = null; - - // Bots - static bots: {[key: number]: Telegraf} = {}; - static botsStates: {[key: number]: BotState} = {}; - static botsPendingUpdatesCount: {[key: number]: number} = {}; - - // Intervals - static syncInterval: NodeJS.Timer | null = null; - - static async start() { - this.launch(); - - await this.sync(); - - if (this.syncInterval === null) { - this.syncInterval = setInterval(() => this.sync(), 300_000); - } - - process.once('SIGINT', () => this.stop()); - process.once('SIGTERM', () => this.stop()); - } - - static async sync() { - const botsData = await makeSyncRequest(); - - if (botsData === null) return; - - await Promise.all(botsData.map((state) => this._updateBotState(state))); - - await Promise.all( - Object.values(this.botsStates).map( - (value: BotState) => this._checkPendingUpdates(this.bots[value.id], value) - ) - ); - } - - static async _updateBotState(state: BotState) { - const isExists = this.bots[state.id] !== undefined; - - if (isExists && - this.botsStates[state.id].status === state.status && - this.botsStates[state.id].cache === state.cache - ) { - return; - } - - try { - const oldBot = new Telegraf(state.token); - await oldBot.telegram.deleteWebhook(); - await oldBot.telegram.logOut(); - } catch (e) {} - - let bot: Telegraf; - - try { - bot = await getBot(state.token, state); - } catch (e) { - return; - } - - if (!(await this._setWebhook(bot, state))) return; - - this.bots[state.id] = bot; - this.botsStates[state.id] = state; - - this.restartApplication(); - } - - static async _checkPendingUpdates(bot: Telegraf, state: BotState) { - try { - const webhookInfo = await bot.telegram.getWebhookInfo(); - const previousPendingUpdateCount = this.botsPendingUpdatesCount[state.id] || 0; - - if (previousPendingUpdateCount !== 0 && webhookInfo.pending_update_count !== 0) { - this._setWebhook(bot, state); - } - - this.botsPendingUpdatesCount[state.id] = webhookInfo.pending_update_count; - } catch (e) { - Sentry.captureException(e, { - extra: { - method: "_checkPendingUpdate", - state_id: state.id, - } - }); - } - } - - static async _setWebhook(bot: Telegraf, state: BotState): Promise { - const dockerIps = (await dockerIpTools.getContainerIp()).split(" "); - const filteredIp = dockerIps.filter((ip) => ip.startsWith(env.NETWORK_IP_PREFIX)); - - const ips = filteredIp.length !== 0 ? filteredIp : dockerIps; - - for (const ip of ips) { - try { - await bot.telegram.setWebhook( - `${env.WEBHOOK_BASE_URL}:${env.WEBHOOK_PORT}/${state.id}/${bot.telegram.token}`, { - ip_address: ip, - } - ); - return true; - } catch (e) {} - } - return false; - } - - static getBotHandlers() { - return Object.keys(this.bots).map((index) => { - const bot = this.bots[parseInt(index)]; - return bot.webhookCallback(`/${index}/${bot.telegram.token}`); - }); - } - - private static async launch() { - const application = express(); - - application.get("/healthcheck", (req, res) => { - res.send("Ok!"); - }); - - application.get("/metrics", (req, res) => { - UsersCounter.getMetrics().then((response) => { - res.send(response); - }); - }); - - const handlers = this.getBotHandlers(); - if (handlers.length !== 0) application.use(handlers); - - this.server = application.listen(env.WEBHOOK_PORT); - - console.log("Server started!"); - } - - static stop() { - Object.keys(this.bots).forEach(key => this.bots[parseInt(key)].telegram.deleteWebhook()); - - if (this.syncInterval) { - clearInterval(this.syncInterval); - this.syncInterval = null; - } - - this.server?.close(); - this.server = null; - - console.log("Server stopped!") - } - - static restartApplication() { - this.server?.close(); - this.server = null; - - this.launch(); - - console.log("Server restarted!"); - } -} diff --git a/src/bots/manager/types.ts b/src/bots/manager/types.ts deleted file mode 100644 index 426d984..0000000 --- a/src/bots/manager/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BotStatuses } from '../factory/index'; - - -export enum Cache { - ORIGINAL = "original", - BUFFER = "buffer", - NO_CACHE = "no_cache" -} - -export interface BotState { - id: number; - token: string; - status: BotStatuses; - cache: Cache; - created_time: string; -} diff --git a/src/bots/manager/utils.ts b/src/bots/manager/utils.ts deleted file mode 100644 index 3577180..0000000 --- a/src/bots/manager/utils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import got from 'got'; - -import env from '@/config'; - -import { BotState } from "./types"; - - -export async function makeSyncRequest(): Promise { - try { - const response = await got(env.MANAGER_URL, { - headers: { - 'Authorization': env.MANAGER_API_KEY - }, - responseType: 'json', - }); - - return response.body; - } catch (err) { - return null; - } -} diff --git a/src/bots/mod.rs b/src/bots/mod.rs new file mode 100644 index 0000000..cc1369e --- /dev/null +++ b/src/bots/mod.rs @@ -0,0 +1,49 @@ +mod approved_bot; + +use std::error::Error; + +use teloxide::prelude::*; + +pub type BotHandlerInternal = Result<(), Box>; + +type BotHandler = Handler< + 'static, + dptree::di::DependencyMap, + BotHandlerInternal, + teloxide::dispatching::DpHandlerDescription, +>; + +type BotCommands = Option>; + +fn get_pending_handler() -> BotHandler { + let handler = |msg: Message, bot: AutoSend| async move { + let message_text = " + Бот зарегистрирован, но не подтвержден администратором! \ + Подтверждение занимает примерно 12 часов. + "; + + bot.send_message(msg.chat.id, message_text).await?; + Ok(()) + }; + + Update::filter_message().chain(dptree::endpoint(handler)) +} + +fn get_blocked_handler() -> BotHandler { + let handler = |msg: Message, bot: AutoSend| async move { + let message_text = "Бот заблокирован!"; + + bot.send_message(msg.chat.id, message_text).await?; + Ok(()) + }; + + Update::filter_message().chain(dptree::endpoint(handler)) +} + +pub fn get_bot_handler(status: crate::bots_manager::BotStatus) -> (BotHandler, BotCommands) { + match status { + crate::bots_manager::BotStatus::Pending => (get_pending_handler(), None), + crate::bots_manager::BotStatus::Approved => approved_bot::get_approved_handler(), + crate::bots_manager::BotStatus::Blocked => (get_blocked_handler(), None), + } +} diff --git a/src/bots_manager.rs b/src/bots_manager.rs new file mode 100644 index 0000000..979c6c2 --- /dev/null +++ b/src/bots_manager.rs @@ -0,0 +1,208 @@ +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +use teloxide::types::BotCommand; +use tokio::time::{sleep, Duration}; + +use teloxide::{ + dispatching::{update_listeners::webhooks, ShutdownToken}, + prelude::*, +}; +use url::Url; + +use serde::Deserialize; + +use crate::config; + +#[derive(Deserialize, Debug, PartialEq, Clone, Copy)] +pub enum BotStatus { + #[serde(rename = "pending")] + Pending, + #[serde(rename = "approved")] + Approved, + #[serde(rename = "blocked")] + Blocked, +} + +#[derive(Deserialize, Debug, PartialEq, Clone, Copy)] +pub enum BotCache { + #[serde(rename = "original")] + Original, + #[serde(rename = "no_cache")] + NoCache, +} + +#[derive(Deserialize, Debug)] +struct BotData { + id: u32, + token: String, + status: BotStatus, + cache: BotCache, +} + +async fn get_bots() -> Result, reqwest::Error> { + let client = reqwest::Client::new(); + let response = client + .get(format!("{}/api", &config::CONFIG.manager_url)) + .header("Authorization", &config::CONFIG.manager_api_key) + .send() + .await; + + match response { + Ok(v) => v.json::>().await, + Err(err) => Err(err), + } +} + +pub struct BotsManager { + next_port: u16, + bot_port_map: HashMap, + bot_status_and_cache_map: HashMap, + bot_shutdown_token_map: HashMap, +} + +impl BotsManager { + pub fn create() -> Self { + BotsManager { + next_port: 8000, + bot_port_map: HashMap::new(), + bot_status_and_cache_map: HashMap::new(), + bot_shutdown_token_map: HashMap::new(), + } + } + + async fn start_bot(&mut self, bot_data: &BotData) { + let bot = Bot::new(bot_data.token.clone()) + .set_api_url(config::CONFIG.telegram_bot_api.clone()) + .auto_send(); + + let token = bot.inner().token(); + let port = self.bot_port_map.get(&bot_data.id).unwrap(); + + let addr = ([0, 0, 0, 0], *port).into(); + + let host = format!("{}:{}", &config::CONFIG.webhook_base_url, port); + let url = Url::parse(&format!("{host}/{token}")).unwrap(); + + log::info!( + "Start bot(id={}) with {:?} handler, port {}", + bot_data.id, + bot_data.status, + port + ); + + let listener = webhooks::axum(bot.clone(), webhooks::Options::new(addr, url)) + .await + .expect("Couldn't setup webhook"); + + let (handler, commands) = crate::bots::get_bot_handler(bot_data.status); + + let set_command_result = match commands { + Some(v) => bot.set_my_commands::>(v).send().await, + None => bot.delete_my_commands().send().await, + }; + match set_command_result { + Ok(_) => (), + Err(err) => log::error!("{:?}", err), + } + + let mut dispatcher = Dispatcher::builder(bot, handler) + .dependencies(dptree::deps![bot_data.cache]) + .build(); + + let shutdown_token = dispatcher.shutdown_token(); + self.bot_shutdown_token_map + .insert(bot_data.id, shutdown_token); + + tokio::spawn(async move { + dispatcher + .dispatch_with_listener( + listener, + LoggingErrorHandler::with_custom_text("An error from the update listener"), + ) + .await; + }); + } + + async fn stop_bot(&mut self, bot_id: u32) { + let shutdown_token = match self.bot_shutdown_token_map.remove(&bot_id) { + Some(v) => v, + None => return, + }; + + for _ in 1..100 { + match shutdown_token.clone().shutdown() { + Ok(v) => return v.await, + Err(_) => (), + }; + + sleep(Duration::from_millis(100)).await; + } + } + + async fn update_data(&mut self, bots_data: Vec) { + for bot_data in bots_data.iter() { + if !self.bot_port_map.contains_key(&bot_data.id) { + self.bot_port_map.insert(bot_data.id, self.next_port); + self.next_port += 1; + } + + match self.bot_status_and_cache_map.get(&bot_data.id) { + Some(v) => { + if *v != (bot_data.status, bot_data.cache) { + self.bot_status_and_cache_map + .insert(bot_data.id, (bot_data.status, bot_data.cache)); + self.stop_bot(bot_data.id).await; + self.start_bot(bot_data).await; + } + } + None => { + self.bot_status_and_cache_map + .insert(bot_data.id, (bot_data.status, bot_data.cache)); + self.start_bot(bot_data).await; + } + } + } + } + + async fn check(&mut self) { + let bots_data = get_bots().await; + + match bots_data { + Ok(v) => self.update_data(v).await, + Err(err) => log::info!("{:?}", err), + } + } + + async fn stop_all(&mut self) { + for token in self.bot_shutdown_token_map.values() { + for _ in 1..100 { + match token.clone().shutdown() { + Ok(v) => { + v.await; + break; + } + Err(_) => (), + } + } + } + } + + pub async fn start(running: Arc) { + let mut manager = BotsManager::create(); + + loop { + manager.check().await; + + for _ in 1..30 { + sleep(Duration::from_secs(1)).await; + + if !running.load(Ordering::SeqCst) { + manager.stop_all().await; + return; + } + } + } + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..3bbc009 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,54 @@ +pub struct Config { + pub telegram_bot_api: reqwest::Url, + + pub webhook_base_url: String, + + pub manager_url: String, + pub manager_api_key: String, + + pub user_settings_url: String, + pub user_settings_api_key: String, + + pub book_server_url: String, + pub book_server_api_key: String, + + pub cache_server_url: String, + pub cache_server_api_key: String, + + pub sentry_dsn: String, +} + +fn get_env(env: &'static str) -> String { + std::env::var(env).unwrap_or_else(|_| panic!("Cannot get the {} env variable", env)) +} + +impl Config { + pub fn load() -> Config { + Config { + telegram_bot_api: reqwest::Url::parse(&get_env("TELEGRAM_BOT_API_ROOT")) + .unwrap_or_else(|_| { + panic!("Cannot parse url from TELEGRAM_BOT_API_ROOT env variable") + }), + + webhook_base_url: get_env("WEBHOOK_BASE_URL"), + + manager_url: get_env("MANAGER_URL"), + manager_api_key: get_env("MANAGER_API_KEY"), + + user_settings_url: get_env("USER_SETTINGS_URL"), + user_settings_api_key: get_env("USER_SETTINGS_API_KEY"), + + book_server_url: get_env("BOOK_SERVER_URL"), + book_server_api_key: get_env("BOOK_SERVER_API_KEY"), + + cache_server_url: get_env("CACHE_SERVER_URL"), + cache_server_api_key: get_env("CACHE_SERVER_API_KEY"), + + sentry_dsn: get_env("SENTRY_DSN"), + } + } +} + +lazy_static! { + pub static ref CONFIG: Config = Config::load(); +} diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index 537a9bd..0000000 --- a/src/config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { cleanEnv, str, num } from 'envalid'; - - -export default cleanEnv(process.env, { - SENTRY_DSN: str(), - - WEBHOOK_BASE_URL: str(), - WEBHOOK_PORT: num(), - - TELEGRAM_BOT_API_ROOT: str({ default: "https://api.telegram.org" }), - - MANAGER_URL: str(), - MANAGER_API_KEY: str(), - - BOOK_SERVER_URL: str(), - BOOK_SERVER_API_KEY: str(), - - CACHE_SERVER_URL: str(), - CACHE_SERVER_API_KEY: str(), - - BUFFER_SERVER_URL: str(), - BUFFER_SERVER_API_KEY: str(), - - DOWNLOADER_URL: str(), - DOWNLOADER_API_KEY: str(), - - USER_SETTINGS_URL: str(), - USER_SETTINGS_API_KEY: str(), - - RATINGS_URL: str(), - RATINGS_API_KEY: str(), - - NETWORK_IP_PREFIX: str(), - - REDIS_HOST: str(), - REDIS_PORT: num(), - REDIS_DB: num(), -}); diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..21695cc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,29 @@ +#[macro_use] +extern crate lazy_static; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; + +mod bots; +mod bots_manager; +mod config; + +#[tokio::main] +async fn main() { + let _guard = sentry::init(config::CONFIG.sentry_dsn.clone()); + env_logger::init(); + + let running = Arc::new(AtomicBool::new(true)); + let r = running.clone(); + + ctrlc::set_handler(move || { + r.store(false, Ordering::SeqCst); + }) + .expect("Error setting Ctrl-C handler"); + + tokio::spawn(async move { + bots_manager::BotsManager::start(running).await; + }) + .await + .unwrap(); +} diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 3bccd5f..0000000 --- a/src/main.ts +++ /dev/null @@ -1,3 +0,0 @@ -import BotsManager from './bots/manager'; - -BotsManager.start(); diff --git a/src/sentry.ts b/src/sentry.ts deleted file mode 100644 index fedf937..0000000 --- a/src/sentry.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Sentry from '@sentry/node'; - -import env from '@/config'; - - -Sentry.init({ - dsn: env.SENTRY_DSN, -}); - -export default Sentry; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 7d0d7de..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Projects */ - // "incremental": true, /* Enable incremental compilation */ - // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ - // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ - // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ - // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ - // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - - /* Language and Environment */ - "target": "ESNEXT", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ - // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ - // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ - // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ - // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ - // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ - // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ - // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ - // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ - // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ - - /* Modules */ - "module": "ESNEXT", /* Specify what module code is generated. */ - // "rootDir": "./src", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ - "baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */ - "paths": { - "@/*": ["./*"] - }, /* Specify a set of entries that re-map imports to additional lookup locations. */ - // "rootDirs": [ - // "./src", - // ], /* Allow multiple folders to be treated as one when resolving modules. */ - // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ - // "types": [], /* Specify type package names to be included without being referenced in a source file. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - // "resolveJsonModule": true, /* Enable importing .json files */ - // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ - - /* JavaScript Support */ - // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ - // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ - // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ - - /* Emit */ - // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ - // "declarationMap": true, /* Create sourcemaps for d.ts files. */ - // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ - // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ - // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ - "outDir": "./build/", /* Specify an output folder for all emitted files. */ - // "removeComments": true, /* Disable emitting comments. */ - // "noEmit": true, /* Disable emitting files from a compilation. */ - // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ - // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ - // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ - // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ - // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ - // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ - // "newLine": "crlf", /* Set the newline character for emitting files. */ - // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ - // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ - // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ - // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ - // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ - - /* Interop Constraints */ - // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ - // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ - // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - - /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ - // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ - // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ - // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ - // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ - // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ - // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ - // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ - // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ - // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ - // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ - // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ - // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - - /* Completeness */ - // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ - } -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index 1c4fc32..0000000 --- a/yarn.lock +++ /dev/null @@ -1,1354 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@esbuild/linux-loong64@0.14.53": - version "0.14.53" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6" - integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg== - -"@redis/bloom@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf" - integrity sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw== - -"@redis/client@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.2.0.tgz#be2ef974881e57276123cb76d08756c03eed946f" - integrity sha512-a8Nlw5fv2EIAFJxTDSSDVUT7yfBGpZO96ybZXzQpgkyLg/dxtQ1uiwTc0EGfzg1mrPjZokeBSEGTbGXekqTNOg== - dependencies: - cluster-key-slot "1.1.0" - generic-pool "3.8.2" - yallist "4.0.0" - -"@redis/graph@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.0.1.tgz#eabc58ba99cd70d0c907169c02b55497e4ec8a99" - integrity sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ== - -"@redis/json@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.3.tgz#a13fde1d22ebff0ae2805cd8e1e70522b08ea866" - integrity sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q== - -"@redis/search@1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.0.6.tgz#53d7451c2783f011ebc48ec4c2891264e0b22f10" - integrity sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA== - -"@redis/time-series@1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.3.tgz#4cfca8e564228c0bddcdf4418cba60c20b224ac4" - integrity sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA== - -"@sentry/core@7.8.1": - version "7.8.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.8.1.tgz#d11ba7c97766d1e47edf697dbfd47fe4041477d9" - integrity sha512-PRivbdIzApi/gSixAxozhOBTylSVdw/9VxaStYHd7JJGhs36KXkV8ylpbCmYO4ap7/Ue9/slzwpvPOJJzmzAgA== - dependencies: - "@sentry/hub" "7.8.1" - "@sentry/types" "7.8.1" - "@sentry/utils" "7.8.1" - tslib "^1.9.3" - -"@sentry/hub@7.8.1": - version "7.8.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-7.8.1.tgz#bc255c6b8e99a3333e737f189c984c715df504aa" - integrity sha512-AxwyGyS9Lp4XsURu4t8opa5vZ+NAB6I/n+B/Uix3YZea9z8jdWYAu9vsXSizOrtxekc/i7ZN4bnlNgXVHix0iA== - dependencies: - "@sentry/types" "7.8.1" - "@sentry/utils" "7.8.1" - tslib "^1.9.3" - -"@sentry/node@^7.8.1": - version "7.8.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.8.1.tgz#9dca7afee2d9ad38fd9756e13439ce09d2f01934" - integrity sha512-gQHeIip7QudeK1YWrLyZPc7nfirhbVDJ3gfCfL9mLT724Sk8gKd1kcpU1niI+wwIwY7SOpJqX4Oh/F0lRKjzDQ== - dependencies: - "@sentry/core" "7.8.1" - "@sentry/hub" "7.8.1" - "@sentry/types" "7.8.1" - "@sentry/utils" "7.8.1" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/types@7.8.1": - version "7.8.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.8.1.tgz#c00a1ed02ad8f69d3b94fcda91e2d24e0bb3492a" - integrity sha512-LOoaeBXVI23Kh5SpIbxSRiJ6+eYZXVOFyPFH1T1mGBj95LPwRMqOdg0lUTmFJGBKbDGDB/YNjNnu1kQ7GrXBXw== - -"@sentry/utils@7.8.1": - version "7.8.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.8.1.tgz#5d8a7e1c8d834de608ad834cf648d5291c62ba39" - integrity sha512-isUZjft4HWTOk1Z58KFJ/zzXeFtIJgP82CkYQlW464ZR2WCqPHYlXXXRWZpOHOfMnrf+gWeX9WAGS9rTAdhiSg== - dependencies: - "@sentry/types" "7.8.1" - tslib "^1.9.3" - -"@sindresorhus/is@^5.2.0": - version "5.3.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-5.3.0.tgz#0ec9264cf54a527671d990eb874e030b55b70dcc" - integrity sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw== - -"@szmarczak/http-timer@^5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-5.0.1.tgz#c7c1bf1141cdd4751b0399c8fc7b8b664cd5be3a" - integrity sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw== - dependencies: - defer-to-connect "^2.0.1" - -"@types/body-parser@*": - version "1.19.2" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" - integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== - dependencies: - "@types/connect" "*" - "@types/node" "*" - -"@types/cacheable-request@^6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.2.tgz#c324da0197de0a98a2312156536ae262429ff6b9" - integrity sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "*" - "@types/node" "*" - "@types/responselike" "*" - -"@types/chunk-text@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/chunk-text/-/chunk-text-1.0.0.tgz#9cfa9b7071fad7029f695e51a8178b769e2fe588" - integrity sha512-TAQeh9qyBGf8HfsvvZ5BTcXEm7hMCm1WGnYDmro1lMHfydjdcVN/X1vLLfhoadKghu+nf5i2o0bLIiQRzZ6EjQ== - -"@types/connect@*": - version "3.4.35" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" - integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== - dependencies: - "@types/node" "*" - -"@types/debug@^4.1.7": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== - dependencies: - "@types/ms" "*" - -"@types/express-serve-static-core@^4.17.18": - version "4.17.28" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz#c47def9f34ec81dc6328d0b1b5303d1ec98d86b8" - integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== - dependencies: - "@types/node" "*" - "@types/qs" "*" - "@types/range-parser" "*" - -"@types/express@^4.17.13": - version "4.17.13" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.13.tgz#a76e2995728999bab51a33fabce1d705a3709034" - integrity sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA== - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "^4.17.18" - "@types/qs" "*" - "@types/serve-static" "*" - -"@types/http-cache-semantics@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" - integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== - -"@types/keyv@*": - version "3.1.3" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.3.tgz#1c9aae32872ec1f20dcdaee89a9f3ba88f465e41" - integrity sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== - dependencies: - "@types/node" "*" - -"@types/mime@^1": - version "1.3.2" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" - integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== - -"@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - -"@types/node@*": - version "17.0.21" - resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.21.tgz#864b987c0c68d07b4345845c3e63b75edd143644" - integrity sha512-DBZCJbhII3r90XbQxI8Y9IjjiiOGlZ0Hr32omXIZvwwZ7p4DMMXGrKXVyPfuoBOri9XNtL0UK69jYIBIsRX3QQ== - -"@types/node@^16.11.9": - version "16.11.47" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.47.tgz#efa9e3e0f72e7aa6a138055dace7437a83d9f91c" - integrity sha512-fpP+jk2zJ4VW66+wAMFoBJlx1bxmBKx4DUFf68UHgdGCOuyUTDlLWqsaNPJh7xhNDykyJ9eIzAygilP/4WoN8g== - -"@types/qs@*": - version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/range-parser@*": - version "1.2.4" - resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" - integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== - -"@types/responselike@*", "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - dependencies: - "@types/node" "*" - -"@types/safe-compare@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@types/safe-compare/-/safe-compare-1.1.0.tgz#47ed9b9ca51a3a791b431cd59b28f47fa9bf1224" - integrity sha512-1ri+LJhh0gRxIa37IpGytdaW7yDEHeJniBSMD1BmitS07R1j63brcYCzry+l0WJvGdEKQNQ7DYXO2epgborWPw== - -"@types/serve-static@*": - version "1.13.10" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.10.tgz#f5e0ce8797d2d7cc5ebeda48a52c96c4fa47a8d9" - integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== - dependencies: - "@types/mime" "^1" - "@types/node" "*" - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -abort-controller@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" - integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== - dependencies: - event-target-shim "^5.0.0" - -accepts@~1.3.8: - version "1.3.8" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" - integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== - dependencies: - mime-types "~2.1.34" - negotiator "0.6.3" - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -array-flatten@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" - integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -body-parser@1.20.0: - version "1.20.0" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" - integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== - dependencies: - bytes "3.1.2" - content-type "~1.0.4" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.10.3" - raw-body "2.5.1" - type-is "~1.6.18" - unpipe "1.0.0" - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -buffer-alloc-unsafe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" - integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== - -buffer-alloc@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" - integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== - dependencies: - buffer-alloc-unsafe "^1.1.0" - buffer-fill "^1.0.0" - -buffer-fill@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" - integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= - -bytes@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" - integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== - -cacheable-lookup@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-6.0.4.tgz#65c0e51721bb7f9f2cb513aed6da4a1b93ad7dc8" - integrity sha512-mbcDEZCkv2CZF4G01kr8eBd/5agkt9oCqz75tJMSIsquvRZ2sL6Hi5zGVKi/0OSC9oO1GHfJ2AV0ZIOY9vye0A== - -cacheable-request@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" - integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^6.0.1" - responselike "^2.0.0" - -call-bind@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -chokidar@^3.5.2: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chunk-text@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/chunk-text/-/chunk-text-2.0.1.tgz#4d6e9946c6b842f823b504ee25366a6644733272" - integrity sha512-ER6TSpe2DT4wjOVOKJ3FFAYv7wE77HA/Ztz88Peiv3lq/2oVMsItYJJsVVI0xNZM8cdImOOTNqlw+LQz7gYdJg== - dependencies: - runes "^0.4.3" - -clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= - dependencies: - mimic-response "^1.0.0" - -cluster-key-slot@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" - integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -content-disposition@0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" - integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== - dependencies: - safe-buffer "5.2.1" - -content-type@~1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" - integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== - -cookie-signature@1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" - integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= - -cookie@0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" - integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== - -cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - -debug@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" - integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== - dependencies: - ms "2.1.2" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -defer-to-connect@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -docker-ip-get@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/docker-ip-get/-/docker-ip-get-1.1.5.tgz#745e75e66dad8280c49f6385a2e76ed91de35a44" - integrity sha512-ZpXdcg5bB86U/3ZMxHSw4rqzCOp7MVMLG8oGbUfTFLKk+qsT+ymNRl7cmv4TTPP3YhL5+aEHuy+qcvJzd3pM0w== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= - -end-of-stream@^1.1.0: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -envalid@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/envalid/-/envalid-7.3.1.tgz#5bf6bbb4effab2d64a1991d8078b4ae38924f0d2" - integrity sha512-KL1YRwn8WcoF/Ty7t+yLLtZol01xr9ZJMTjzoGRM8NaSU+nQQjSWOQKKJhJP2P57bpdakJ9jbxqQX4fGTOicZg== - dependencies: - tslib "2.3.1" - -esbuild-android-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675" - integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA== - -esbuild-android-arm64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e" - integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A== - -esbuild-darwin-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a" - integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg== - -esbuild-darwin-arm64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9" - integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA== - -esbuild-freebsd-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0" - integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w== - -esbuild-freebsd-arm64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9" - integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ== - -esbuild-linux-32@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5" - integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg== - -esbuild-linux-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4" - integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ== - -esbuild-linux-arm64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe" - integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw== - -esbuild-linux-arm@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf" - integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA== - -esbuild-linux-mips64le@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f" - integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ== - -esbuild-linux-ppc64le@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139" - integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA== - -esbuild-linux-riscv64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00" - integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ== - -esbuild-linux-s390x@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129" - integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg== - -esbuild-netbsd-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52" - integrity sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ== - -esbuild-openbsd-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b" - integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ== - -esbuild-sunos-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8" - integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g== - -esbuild-windows-32@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709" - integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg== - -esbuild-windows-64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af" - integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ== - -esbuild-windows-arm64@0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8" - integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ== - -esbuild@^0.14.53: - version "0.14.53" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4" - integrity sha512-ohO33pUBQ64q6mmheX1mZ8mIXj8ivQY/L4oVuAshr+aJI+zLl+amrp3EodrUNDNYVrKJXGPfIHFGhO8slGRjuw== - optionalDependencies: - "@esbuild/linux-loong64" "0.14.53" - esbuild-android-64 "0.14.53" - esbuild-android-arm64 "0.14.53" - esbuild-darwin-64 "0.14.53" - esbuild-darwin-arm64 "0.14.53" - esbuild-freebsd-64 "0.14.53" - esbuild-freebsd-arm64 "0.14.53" - esbuild-linux-32 "0.14.53" - esbuild-linux-64 "0.14.53" - esbuild-linux-arm "0.14.53" - esbuild-linux-arm64 "0.14.53" - esbuild-linux-mips64le "0.14.53" - esbuild-linux-ppc64le "0.14.53" - esbuild-linux-riscv64 "0.14.53" - esbuild-linux-s390x "0.14.53" - esbuild-netbsd-64 "0.14.53" - esbuild-openbsd-64 "0.14.53" - esbuild-sunos-64 "0.14.53" - esbuild-windows-32 "0.14.53" - esbuild-windows-64 "0.14.53" - esbuild-windows-arm64 "0.14.53" - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= - -etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= - -event-target-shim@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" - integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== - -express@^4.18.1: - version "4.18.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" - integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== - dependencies: - accepts "~1.3.8" - array-flatten "1.1.1" - body-parser "1.20.0" - content-disposition "0.5.4" - content-type "~1.0.4" - cookie "0.5.0" - cookie-signature "1.0.6" - debug "2.6.9" - depd "2.0.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - finalhandler "1.2.0" - fresh "0.5.2" - http-errors "2.0.0" - merge-descriptors "1.0.1" - methods "~1.1.2" - on-finished "2.4.1" - parseurl "~1.3.3" - path-to-regexp "0.1.7" - proxy-addr "~2.0.7" - qs "6.10.3" - range-parser "~1.2.1" - safe-buffer "5.2.1" - send "0.18.0" - serve-static "1.15.0" - setprototypeof "1.2.0" - statuses "2.0.1" - type-is "~1.6.18" - utils-merge "1.0.1" - vary "~1.1.2" - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -finalhandler@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" - integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== - dependencies: - debug "2.6.9" - encodeurl "~1.0.2" - escape-html "~1.0.3" - on-finished "2.4.1" - parseurl "~1.3.3" - statuses "2.0.1" - unpipe "~1.0.0" - -form-data-encoder@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/form-data-encoder/-/form-data-encoder-2.0.1.tgz#aec41860aca0275cb6026650d139c6701b0992c1" - integrity sha512-Oy+P9w5mnO4TWXVgUiQvggNKPI9/ummcSt5usuIV6HkaLKigwzPpoenhEqmGmx3zHqm6ZLJ+CR/99N8JLinaEw== - -forwarded@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" - integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= - -fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -generic-pool@3.8.2: - version "3.8.2" - resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" - integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== - -get-intrinsic@^1.0.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" - integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -got@^12.3.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/got/-/got-12.3.0.tgz#744625bcb072e7b1fd41a706e0af2bd1f73a2c64" - integrity sha512-7uK06aluHF0UibYFBX3lFUZ2FG/W0KS4O4EqAIrbWIdbPxIT33r6ZJy7Zy+pdh0CP/ZbF3zBa7Fd9dCn7vGPBg== - dependencies: - "@sindresorhus/is" "^5.2.0" - "@szmarczak/http-timer" "^5.0.1" - "@types/cacheable-request" "^6.0.2" - "@types/responselike" "^1.0.0" - cacheable-lookup "^6.0.4" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - form-data-encoder "^2.0.1" - get-stream "^6.0.1" - http2-wrapper "^2.1.10" - lowercase-keys "^3.0.0" - p-cancelable "^3.0.0" - responselike "^2.0.0" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -http-cache-semantics@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http2-wrapper@^2.1.10: - version "2.1.11" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-2.1.11.tgz#d7c980c7ffb85be3859b6a96c800b2951ae257ef" - integrity sha512-aNAk5JzLturWEUiuhAN73Jcbq96R7rTitAoXV54FYMatvihnpD2+6PUgU4ce3D/m5VDbw+F5CsyKSF176ptitQ== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.2.0" - -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -iconv-lite@0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore-by-default@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" - integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= - -inherits@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ipaddr.js@1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" - integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-glob@^4.0.1, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -js-base64@^3.7.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.2.tgz#816d11d81a8aff241603d19ce5761e13e41d7745" - integrity sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -keyv@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.1.1.tgz#02c538bfdbd2a9308cc932d4096f05ae42bfa06a" - integrity sha512-tGv1yP6snQVDSM4X6yxrv2zzq/EvpW+oYiUz6aueW1u9CtS8RzUQYxxmFwgZlO2jSgCxQbchhxaqXXp2hnKGpQ== - dependencies: - json-buffer "3.0.1" - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lowercase-keys@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-3.0.0.tgz#c5e7d442e37ead247ae9db117a9d0a467c89d4f2" - integrity sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ== - -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - -media-typer@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" - integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= - -merge-descriptors@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" - integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= - -methods@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" - integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= - -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@~2.1.24, mime-types@~2.1.34: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimatch@^3.0.4: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== - -module-alias@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/module-alias/-/module-alias-2.2.2.tgz#151cdcecc24e25739ff0aa6e51e1c5716974c0e0" - integrity sha512-A/78XjoX2EmNvppVWEhM2oGk3x4lLxnkEA4jTbaK97QKSDjkIoOsKQlfylt/d3kKKi596Qy3NP5XrXJ6fZIC9Q== - -moment@^2.29.4: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" - integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3, ms@^2.1.1: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -negotiator@0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" - integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== - -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -nodemon@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-2.0.19.tgz#cac175f74b9cb8b57e770d47841995eebe4488bd" - integrity sha512-4pv1f2bMDj0Eeg/MhGqxrtveeQ5/G/UVe9iO6uTZzjnRluSA4PVWf8CW99LUPwGB3eNIA7zUFoP77YuI7hOc0A== - dependencies: - chokidar "^3.5.2" - debug "^3.2.7" - ignore-by-default "^1.0.1" - minimatch "^3.0.4" - pstree.remy "^1.1.8" - semver "^5.7.1" - simple-update-notifier "^1.0.7" - supports-color "^5.5.0" - touch "^3.1.0" - undefsafe "^2.0.5" - -nopt@~1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -object-inspect@^1.9.0: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -p-cancelable@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-3.0.0.tgz#63826694b54d61ca1c20ebcb6d3ecf5e14cd8050" - integrity sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw== - -p-timeout@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-4.1.0.tgz#788253c0452ab0ffecf18a62dff94ff1bd09ca0a" - integrity sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw== - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -path-to-regexp@0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" - integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= - -picomatch@^2.0.4, picomatch@^2.2.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -proxy-addr@~2.0.7: - version "2.0.7" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" - integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== - dependencies: - forwarded "0.2.0" - ipaddr.js "1.9.1" - -pstree.remy@^1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.8.tgz#c242224f4a67c21f686839bbdb4ac282b8373d3a" - integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -qs@6.10.3: - version "6.10.3" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" - integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== - dependencies: - side-channel "^1.0.4" - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -raw-body@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" - integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -redis@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/redis/-/redis-4.2.0.tgz#1278a265b8aa1e096a585d103bdead027cd04e43" - integrity sha512-bCR0gKVhIXFg8zCQjXEANzgI01DDixtPZgIUZHBCmwqixnu+MK3Tb2yqGjh+HCLASQVVgApiwhNkv+FoedZOGQ== - dependencies: - "@redis/bloom" "1.0.2" - "@redis/client" "1.2.0" - "@redis/graph" "1.0.1" - "@redis/json" "1.0.3" - "@redis/search" "1.0.6" - "@redis/time-series" "1.0.3" - -resolve-alpn@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -responselike@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.0.tgz#26391bcc3174f750f9a79eacc40a12a5c42d7723" - integrity sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw== - dependencies: - lowercase-keys "^2.0.0" - -runes@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/runes/-/runes-0.4.3.tgz#32f7738844bc767b65cc68171528e3373c7bb355" - integrity sha512-K6p9y4ZyL9wPzA+PMDloNQPfoDGTiFYDvdlXznyGKgD10BJpcAosvATKrExRKOrNLgD8E7Um7WGW0lxsnOuNLg== - -safe-buffer@5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-compare@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/safe-compare/-/safe-compare-1.1.4.tgz#5e0128538a82820e2e9250cd78e45da6786ba593" - integrity sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ== - dependencies: - buffer-alloc "^1.2.0" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -sandwich-stream@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/sandwich-stream/-/sandwich-stream-2.0.2.tgz#6d1feb6cf7e9fe9fadb41513459a72c2e84000fa" - integrity sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ== - -semver@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@~7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" - integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -serve-static@1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -side-channel@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" - integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== - dependencies: - call-bind "^1.0.0" - get-intrinsic "^1.0.2" - object-inspect "^1.9.0" - -simple-update-notifier@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz#7edf75c5bdd04f88828d632f762b2bc32996a9cc" - integrity sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew== - dependencies: - semver "~7.0.0" - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -supports-color@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -telegraf@^4.8.6: - version "4.8.6" - resolved "https://registry.yarnpkg.com/telegraf/-/telegraf-4.8.6.tgz#6a127be631469effbd41c782dd7fe699ae07f0c4" - integrity sha512-FixxnJBrS8ECk/Wmo3VXzkTOlY2A1gsdIORJ//cdh3PcVqAL7wgcvLKjkEPI0IyxdFfRqTrWQEOn9h++revQaA== - dependencies: - abort-controller "^3.0.0" - debug "^4.3.3" - minimist "^1.2.6" - module-alias "^2.2.2" - node-fetch "^2.6.7" - p-timeout "^4.1.0" - safe-compare "^1.1.4" - sandwich-stream "^2.0.2" - typegram "^3.9.0" - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -touch@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/touch/-/touch-3.1.0.tgz#fe365f5f75ec9ed4e56825e0bb76d24ab74af83b" - integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== - dependencies: - nopt "~1.0.10" - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= - -tslib@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" - integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== - -tslib@^1.9.3: - version "1.14.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" - integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== - -type-is@~1.6.18: - version "1.6.18" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" - integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== - dependencies: - media-typer "0.3.0" - mime-types "~2.1.24" - -typegram@^3.9.0: - version "3.10.0" - resolved "https://registry.yarnpkg.com/typegram/-/typegram-3.10.0.tgz#8f02c2a7748f019f0566279318d2be396d103bd8" - integrity sha512-kma7ZF7SFRqcUCgo5sHg1MbPwc9/KYjVkbvrqIZK7oXfPdLBGz1s7wF9d7o4yjHp+AOGke8cyYGhI/+4xYYC4Q== - -typescript@^4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== - -undefsafe@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c" - integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== - -unpipe@1.0.0, unpipe@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" - integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= - -utils-merge@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" - integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= - -vary@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" - integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -yallist@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==