commit 07277145d4e484da68b53e0ad0681ff81468c6c9 Author: ihciah Date: Sun Apr 10 01:51:04 2022 +0800 init diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..426747e --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "unsound_local_offset"] \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e174828 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +/target/ +/.git/ \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..a39b4a9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + paths-ignore: + - '**.md' + - '**.png' + pull_request: + paths-ignore: + - '**.md' + - '**.png' + +env: + RUST_TOOLCHAIN: nightly + TOOLCHAIN_PROFILE: minimal + +jobs: + lints: + name: Run cargo fmt and cargo clippy + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + components: rustfmt, clippy + - name: Cache + uses: Swatinem/rust-cache@v1 + - name: Run cargo fmt + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Run cargo check with no default features + uses: actions-rs/cargo@v1 + with: + command: check + args: --no-default-features + - name: Run cargo check with all features + uses: actions-rs/cargo@v1 + with: + command: check + args: --all-features + - name: Run cargo clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- -D warnings + test: + name: Run cargo test + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Install toolchain + uses: actions-rs/toolchain@v1 + with: + profile: ${{ env.TOOLCHAIN_PROFILE }} + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + - name: Cache + uses: Swatinem/rust-cache@v1 + - name: Run cargo test --no-run + uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --no-run + - name: Run cargo test + run: sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && sudo -u runner RUSTUP_TOOLCHAIN=nightly /home/runner/.cargo/bin/cargo test --all-features" diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..2501182 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,43 @@ +name: docker build and push + +on: + push: + tags: + - 'v*' + +jobs: + build: + name: 'Build' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Extract tag + id: prep + if: "startsWith(github.ref, 'refs/tags/v')" + run: | + echo ::set-output name=tags::ghcr.io/qini7-sese/ehbot:${GITHUB_REF#refs/tags/v} + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + - name: Login to GHCR + uses: docker/login-action@v1 + with: + registry: ghcr.io + username: qini7-sese + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build docker image + uses: docker/build-push-action@v2 + with: + push: true + tags: | + ghcr.io/qini7-sese/ehbot:amd64 + ${{ steps.prep.outputs.tags }} + - name: Docker manifest push + run: | + docker manifest create ghcr.io/qini7-sese/ehbot:latest ghcr.io/qini7-sese/ehbot:amd64 + docker manifest push ghcr.io/qini7-sese/ehbot:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..59b2d02 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +config.yaml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0483a0b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2291 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "rand 0.7.3", + "wasm-timer", +] + +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.6", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" + +[[package]] +name = "aquamarine" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e14cb2a51c8b45d26a4219981985c7350fc05eacb7b5b2939bceb2ffefdf3e" +dependencies = [ + "itertools", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +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 = "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 = "bot" +version = "0.1.3" +dependencies = [ + "anyhow", + "clap", + "dptree", + "eh2telegraph", + "once_cell", + "regex", + "reqwest", + "serde", + "singleflight-async", + "teloxide", + "time", + "tokio", + "tracing", + "tracing-subscriber", + "vergen", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[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.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "clap" +version = "3.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c47df61d9e16dc010b55dba1952a57d8c215dbb533fd13cdd13369aac73b1c" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static", + "os_str_bytes", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aab4734e083b809aaf5794e14e756d1c798d2c69c7f7de7a09a2f5214993c1" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "cloudflare-kv-proxy" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1896e84b3eba9e6957cdb2b8bc4bc43e1ee56ce688377b3f0570d8926e955b7" +dependencies = [ + "coarsetime", + "hashlink 0.7.0", + "parking_lot 0.12.0", + "reqwest", + "serde", + "serde_json", + "serde_with", + "thiserror", +] + +[[package]] +name = "coarsetime" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" +dependencies = [ + "libc", + "once_cell", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[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 = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", +] + +[[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 = "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.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7fcc437df15b844a38de424170425a6e8b52c79e22364fec6133bcdf18c0abd" +dependencies = [ + "futures", +] + +[[package]] +name = "eh2telegraph" +version = "0.1.0" +dependencies = [ + "again", + "anyhow", + "bytes", + "clap", + "cloudflare-kv-proxy", + "derive_more", + "futures", + "hashlink 0.8.0", + "ipnet", + "lazy_static", + "once_cell", + "parking_lot 0.12.0", + "rand 0.8.5", + "regex", + "reqwest", + "rustls", + "serde", + "serde_with", + "serde_yaml", + "thiserror", + "tokio", + "tracing", + "webpki", + "webpki-roots", +] + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "enum-iterator" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "flurry" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c0a35f7b50e99185a2825541946252f669f3c3ca77801357cd682a1b356bb3e" +dependencies = [ + "ahash 0.3.8", + "crossbeam-epoch", + "num_cpus", + "parking_lot 0.10.2", +] + +[[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.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +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.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "getset" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.7.1", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c21d40587b92fa6a6c6e3c1bdbf87d75511db5672f9c93175574b3a00df1758" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hashlink" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452c155cb93fecdfb02a73dd57b5d8e442c2063bd7aac72f1bc5e4263a43086" +dependencies = [ + "hashbrown 0.12.0", +] + +[[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 = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +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-rustls" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +dependencies = [ + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[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 = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +dependencies = [ + "autocfg", + "hashbrown 0.11.2", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "ipnet" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +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.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec647867e2bf0772e28c8bcde4f0d19a9216916e890543b5a03ed8ef27b8f259" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +dependencies = [ + "cfg-if 1.0.0", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + +[[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 = "mio" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[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 = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aba1801fb138d8e85e11d0fc70baf4fe1cdfffda7c6cd34a854905df588e5ed0" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if 1.0.0", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[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.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "parking_lot" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a704eb390aafdc107b0e392f56a82b668e3a71366993b5340f5833fd62505e" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.7.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api 0.4.7", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api 0.4.7", + "parking_lot_core 0.9.2", +] + +[[package]] +name = "parking_lot_core" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d58c7c768d4ba344e3e8d72518ac13e259d7c7ade24167003b8488e10b6740a3" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if 1.0.0", + "instant", + "libc", + "redox_syscall 0.2.13", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "995f667a6c822200b0433ac218e05582f0e2efa1b922a3fd2fbaadc5f87bab37" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.13", + "smallvec", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[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.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "632d02bff7f874a36f33ea8bb416cd484b90cc66c1194b1a1110d067a7013f58" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom 0.2.6", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[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.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util 0.6.9", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static", + "winapi", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65bd28f48be7196d222d95b9243287f48d27aca604e08497513019ff0502cc4" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +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" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946fa04a8ac43ff78a1f4b811990afb9ddbdf5890b46d6dda0ba1998230138b7" +dependencies = [ + "rustversion", + "serde", + "serde_json", + "serde_with_macros", +] + +[[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 = "serde_yaml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[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 = "singleflight-async" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a83e05d4eefa41745bd11e29c0020d62e4f4bd1db4dc24917a37b00848ea861" +dependencies = [ + "parking_lot 0.12.0", + "tokio", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[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.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0471ee22325fa9d4d0574afb21104c28466dbd02495dbd5fef2d431c58c7a5" +dependencies = [ + "aquamarine", + "async-trait", + "bytes", + "derive_more", + "dptree", + "flurry", + "futures", + "log", + "mime", + "pin-project", + "serde", + "serde_json", + "serde_with_macros", + "teloxide-core", + "teloxide-macros", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util 0.6.9", +] + +[[package]] +name = "teloxide-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc4d7ef8a6e39097e49a2481169fa9fc0ff627d8321a7ab52cf6d5ac47850fff" +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 0.6.9", + "url", + "uuid", +] + +[[package]] +name = "teloxide-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d08322f107110dc4aadf5683bf19df9340eebc567529def2d17c830de198a58" +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 1.0.0", + "fastrand", + "libc", + "redox_syscall 0.2.13", + "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" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "parking_lot 0.12.0", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +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-rustls" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0edfdeb067411dba2044da6d1cb2df793dd35add7888d73c16e3381ded401764" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +dependencies = [ + "cfg-if 1.0.0", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90442985ee2f57c9e1b548ee72ae842f4a9a20e3f417cc38dbc5dc684d9bb4ee" +dependencies = [ + "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9df98b037d039d03400d9dd06b0f8ce05486b5f25e9a2d7d36196e142ebbc52" +dependencies = [ + "ansi_term", + "parking_lot 0.12.0", + "sharded-slab", + "smallvec", + "thread_local", + "time", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[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.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", + "serde", +] + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.6", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "7.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db743914c971db162f35bf46601c5a63ec4452e61461937b4c1ab817a60c12e" +dependencies = [ + "anyhow", + "cfg-if 1.0.0", + "enum-iterator", + "getset", + "rustc_version", + "rustversion", + "thiserror", + "time", +] + +[[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.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[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.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" +dependencies = [ + "webpki", +] + +[[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.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" +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.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" + +[[package]] +name = "windows_i686_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" + +[[package]] +name = "windows_i686_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0db099a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[workspace] +members = [ + "bot", + "eh2telegraph", +] + +[profile.release] +lto = true +opt-level = 3 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..24aed1a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM rust:1-bullseye as builder +WORKDIR /usr/src/eh2telegraph +COPY . . +RUN cargo build --release + +FROM debian:bullseye-slim +RUN apt-get update && apt-get -y install ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /usr/src/eh2telegraph/target/release/bot /usr/local/bin/bot +CMD ["/usr/local/bin/bot"] \ No newline at end of file diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 0000000..59c2796 --- /dev/null +++ b/README-zh.md @@ -0,0 +1,92 @@ +# eh2telegraph + +中文|[英文](README.md) + +自动从 EH/EX/NH 下载图片集并上传至 Telegraph 的 Bot。 + +本代码只保证在 MacOS(部分功能)和 Linux 上可以正确运行。 + +## 部署指引 +1. 安装 Docker 和 docker-compose。 +2. 创建新文件夹 `ehbot`。 +2. 复制项目中的 `config_example.yaml` 至 `ehbot` 并重命名为 `config.yaml`,之后修改配置细节(请参考下一节)。 +3. 复制 `docker-compose.yml` 至 `ehbot`。 +4. 开启与关闭: + 1. 开启:在该路径中运行 `docker-compose up -d`。 + 2. 关闭:在该路径中运行 `docker-compose down`。 + 3. 查看日志:在该路径中运行 `docker-compose logs`。 + 4. 更新镜像:在该路径中运行 `docker-compose pull`。 + +## 配置指引 +1. 基础配置: + 1. Bot Token:Telegram 内找 @BotFather 申请。 + 2. Admin(可空):你的 Telegram ID,随便找个相关 Bot 就可以拿到(也可以通过本 Bot `/id` 拿到)。 + 3. Telegraph:使用浏览器通过[这个链接](https://api.telegra.ph/createAccount?short_name=test_account&author_name=test_author)创建 Telegraph Token 并填写。你也可以修改作者名字和 URL。 +2. 代理配置: + 1. 部署本仓库中的 `worker/web_proxy.js` 至 CloudFlare Workers,并配置 `KEY` 环境变量为一段随机字符串(该 KEY 目的是防止对代理的未授权请求)。 + 2. 填写 URL 和 Key 到配置中。 + 3. 该代理用于请求一些有频率限制的服务,请勿滥用。 +3. IPv6 配置: + 1. 可以填写一个 IPv6 段,如果你并没有拥有一个较大的(指比 `/64` 大)IPv6 段,请留空。 + 2. 填写的话需要开启 `net.ipv6.ip_nonlocal_bind` 内核参数(参考后续章节说明)。 + 3. 配置 IPv6 可以一定程度上缓解针对单 IP 的限流。 +4. 配置部分 Collector 的 Cookie: + 1. 目前只有 exhentai 需要。 +5. KV 配置: + 1. 本项目内置使用了一个缓存服务,可以避免对一个图片集的重复同步。 + 2. 请参考 [cloudflare-kv-proxy](https://github.com/ihciah/cloudflare-kv-proxy) 进行部署,并填写至配置文件。 + 3. 如果不想使用远程缓存,也可以使用纯内存缓存(重启后会失效),需要自行改代码并重新编译。 + +## 开发指引 +### 环境 +需要 Rust 最新的 Nightly 版本。推荐使用 VSCode 或 Clion 开发。 + +中国大陆推荐使用 [RsProxy](https://rsproxy.cn/) 作为 crates.io 镜像与工具链安装源。 + +### 版本发布 +打 `v` 开头的 Tag 即可触发 Docker 构建。你可以直接在 git 中打 tag 之后 push 上去;但更方便的是在 github 中发布 release,并填写 `v` 开头的命名。 + +## 技术细节 +虽然本项目就是一个简单的爬虫,但是还是有一些注意事项需要说明一下。 + +### Github Action 构建 +Github Action 可以用于自动构建 Docker 镜像,本项目支持自动构建 `x86_64` 平台的版本。 + +但事实上也可以构建 `arm64` 的版本,由于其机制上使用了 qemu 在 x86_64 上模拟了 arm 环境,所以速度极其缓慢(单次构建需要 1h 以上),于是没有开启。 + +### IPv6 幽灵客户端(口胡的名字) +某些网站有针对 IP 的访问频率限制,使用多个 IP 即可缓解该限制。实践上最常用的办法是代理池,但代理池往往极不稳定,并需要维护,可能还有一定成本。 + +观察本项目的目标网站,很多使用了 Cloudflare,而 Cloudflare 支持 IPv6 且限流粒度是 `/64`。如果我们为本机绑定一个更大的 IPv6 段并从中随机选择 IP 作为客户端出口地址,则可以稳定地进行更高频率的请求。 + +由于网卡只会绑定单个 IPv6 地址,所以我们需要开启 `net.ipv6.ip_nonlocal_bind`。 + +配置 IPv6 后,对于可以走 IPv6 的目标站点,本项目会使用 IPv6 段中的随机 IP 请求。 + +配置(对网卡的配置可以写在 `if-up` 中便于持久化): +1. `sudo ip add add local 2001:x:x::/48 dev lo` +2. `sudo ip route add local 2001:x:x::/48 dev your-interface` +3. 在 Sysctl 中配置 `net.ipv6.ip_nonlocal_bind=1`。该步骤因发行版而异(比如常见的 `/etc/sysctl.conf` 在 Arch Linux 中不存在)。 + +去哪搞 IPv6?he.net 提供了相关免费服务,当然自己购买一个 IPv6 IP 段也并不昂贵。 + +你可以通过 `curl --interface 2001:***** ifconfig.co` 测试配置是否正确。 + +### 强制 IPv6 +前一小节提到的网站虽然用了 Cloudflare,但是事实上并没有真正启用 IPv6。当你直接使用 curl 指定 ipv6 请求时会发现,它根本就没有 AAAA 记录。但是由于 CF 的基础设施是 Anycast 的,所以如果目标网站不在代码中明确地拒绝 IPv6 访客,它们还是可以通过 IPv6 访问的。 + +1. telegra.ph: 无 AAAA 记录,但是强制解析到 Telegram 的入口 IP 可以访问,但证书是 `*.telegram.org` 的。 + + ~~本项目写了一个校验指定域名证书有效性的 TLS 验证器,用于在保证安全性的情况下允许其证书配置错误。~~ + + 但是 Telegraph 以极快的速度修掉了该问题,所以该 TLS 校验器目前处于禁用状态。 +2. EH/NH: 强制 IPv6 可用。 +3. EX: 未使用 CF 且无 IPv6 服务。 + +### 代理 +本项目使用 Cloudflare Workers 作为部分 API 代理,在 IPv6 不可用时缓解限流问题。参考 `src/http_proxy.rs` 和 `worker/web_proxy.js`。 + +### 缓存 +为了尽可能少地重复拉取,本项目使用了内存缓存与远程持久化缓存。远程持久化缓存使用 Cloudflare Worker 配合 Cloudflare KV 搭建。项目主代码参考 [cloudflare-kv-proxy](https://github.com/ihciah/cloudflare-kv-proxy)。 + +由于同步图片集需要一定时间,为了避免重复同步,本项目使用了 [singleflight-async](https://github.com/ihciah/singleflight-async) 减少这类浪费。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..4cf5257 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# eh2telegraph + +[中文](README-zh.md)|英文 + +Bot that automatically downloads image sets from EH/EX/NH and uploads them to Telegraph. + +This code is only guaranteed to work correctly on MacOS (partial functionality) and Linux. + +## Deployment Guidelines +1. Install Docker and docker-compose. +2. Create a new folder `ehbot`. +2. Copy `config_example.yaml` from the project to `ehbot` and rename it to `config.yaml`, then change the configuration details (see the next section). +3. Copy `docker-compose.yml` to `ehbot`. +4. Start and Shutdown. + 1. Start: Run `docker-compose up -d` in this folder. + 2. Shutdown: Run `docker-compose down` in this folder. + 3. View logs: Run `docker-compose logs` in this folder. + 4. Update the image: Run `docker-compose pull` in this folder. + +## Configuration Guidelines +1. Basic Configuration + Bot Token: Find @BotFather in Telegram to apply. + 2. Admin (can be empty): your Telegram ID, you can get it from any relevant Bot (you can also get it from this Bot `/id`). + 3. Telegraph: Use your browser to create a Telegraph Token via [this link](https://api.telegra.ph/createAccount?short_name=test_account&author_name=test_author) and fill in. You can also change the author name and URL. +2. Proxy Configuration + 1. Deploy `worker/web_proxy.js` of this repository to Cloudflare Workers and configure the `KEY` environment variable to be a random string (the purpose of the `KEY` is to prevent unauthorized requests to the proxy). + 2. Fill in the URL and Key into the yaml. + 3. The proxy is used to request some services with frequency limitation, so do not abuse it. +3. IPv6 configuration + 1. You can specify an IPv6 segment, if you do not have a larger (meaning larger than `/64`) IPv6 segment, please leave it blank. + 2. Configure IPv6 to somewhat alleviate the flow restriction for single IP. +4. Configure cookies for some Collectors. + 1. Currently, only exhentai is required. +5. KV configuration + 1. This project uses a built-in caching service to avoid repeated synchronization of an image set. + 2. Please refer to [cloudflare-kv-proxy](https://github.com/ihciah/cloudflare-kv-proxy) for deployment and fill in the yaml file. + 3. If you don't want to use remote caching, you can also use pure memory caching (it will be invalid after reboot). If you want to do so, you need to modify the code and recompile it by yourself. + +## Development Guidelines +### Environment +Requires the latest Nightly version of Rust. Recommended to use VSCode or Clion for development. + +[RsProxy](https://rsproxy.cn/) is recommended as the crates.io source and toolchain installation source for users in China Mainland. + +### Version Release +A Docker build can be triggered by typing a Tag starting with `v`. You can type the tag directly in git and push it up; however, it is easier to publish the release in github and fill in the `v` prefix. + +## Technical Details +Although this project is a simple crawler, there are still some considerations that need to be explained. + +### Github Action Builds +Github Action can be used to automatically build Docker images, and this project supports automatic builds for the `x86_64` platform. + +However, it can also build `arm64` versions, but it is not enabled because it uses qemu to emulate the arm environment on x86_64, so it is extremely slow (more than 1h for a single build). + +### IPv6 Ghost Client (it's not a well-known name, just made up by myself) +Some sites have IP-specific access frequency limits, which can be mitigated by using multiple IPs. The most common approach in practice is proxy pooling, but proxy pools are often extremely unstable and require maintenance and possibly some cost. + +Observe the target sites of this project, many use Cloudflare, and Cloudflare supports IPv6 and the granularity of flow limitation is `/64`. If we bind a larger IPv6 segment for the local machine and randomly select IPs from it as client exit addresses, we can make more frequent requests steadily. + +Since the NIC will only bind a single IPv6 address, we need to enable `net.ipv6.ip_nonlocal_bind`. + +After configuring IPv6, for target sites that can use IPv6, this project will use random IP requests from the IPv6 segment. + +Configuration (configuration for the NIC can be written in `if-up` for persistence). +1. `sudo ip add add local 2001:x:x::/48 dev lo` +2. `sudo ip route add local 2001:x:x::/48 dev your-interface` +3. Configure `net.ipv6.ip_nonlocal_bind=1` in Sysctl. This step varies by distribution (for example, the common `/etc/sysctl.conf` does not exist in Arch Linux). + +Where to get IPv6? he.net offers a free service for this, but of course it is not expensive to buy an IPv6 IP segment yourself. + +You can test the configuration with `curl --interface 2001:***** ifconfig.co` to see if it is correct. + +### Forcing IPv6 +The site mentioned in the previous subsection uses Cloudflare, but in fact does not really enable IPv6. when you specify the ipv6 request directly using curl, you will find that it has no AAAA records at all. But because the CF infrastructure is Anycast, so if the target site does not explicitly deny IPv6 visitors in the code, they can still be accessed through IPv6. + +1. telegra.ph: No AAAA records, but force resolves to Telegram's entry IP for access, but the certificate is `*.telegram.org`. + + ~~This project writes a TLS validator that checks the validity of a given domain's certificate, to allow for misconfiguration of its certificate while maintaining security.~~ + + However, Telegraph fixed the problem very quickly, so the TLS verifier is currently disabled. +2. EH/NH: Forced IPv6 availability. +3. EX: CF is not used and no IPv6 service is available. + +### Proxy +This project uses Cloudflare Workers as a partial API proxy to alleviate the flow limitation problem when IPv6 is not available. See `src/http_proxy.rs` and `worker/web_proxy.js`. + +### Caching +To minimize duplicate pulls, this project uses in-memory caching and remote persistent caching. Remote persistent cache using Cloudflare Worker with Cloudflare KV to build. The main project code reference is [cloudflare-kv-proxy](https://github.com/ihciah/cloudflare-kv-proxy). + +Since it takes some time to synchronize image sets, to avoid repeated synchronization, this project uses [singleflight-async](https://github.com/ihciah/singleflight-async) to reduce this kind of waste. + + +Translated with www.DeepL.com/Translator (free version) \ No newline at end of file diff --git a/bot/Cargo.toml b/bot/Cargo.toml new file mode 100644 index 0000000..1efc24d --- /dev/null +++ b/bot/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2021" +name = "bot" +version = "0.1.3" + +[dependencies] +eh2telegraph = {path = "../eh2telegraph"} + +anyhow = "1" +clap = {version = "3", features = ["derive"]} +dptree = "0.1" +once_cell = "1" +regex = "1" +reqwest = {version = "0.11", default-features = false, features = ["json", "multipart", "rustls-tls"]} +serde = {version = "1", features = ["derive"]} +singleflight-async = {version = "0.1", features = ["hardware-lock-elision"]} +teloxide = {version = "0.7", features = ["macros", "ctrlc_handler", "dispatching2", "auto-send"]} +time = {version = "0.3", features = ["local-offset", "std", "macros"]} +tokio = {version = "1", default-features = false, features = ["rt-multi-thread", "macros", "net", "sync", "time", "parking_lot"]} +tracing = "0.1" +tracing-subscriber = {version = "0.3", features = ["local-time", "parking_lot", "time"]} + +[build-dependencies] +vergen = {version = "7", default_features = false, features = ["build", "cargo", "rustc"]} diff --git a/bot/build.rs b/bot/build.rs new file mode 100644 index 0000000..6b29072 --- /dev/null +++ b/bot/build.rs @@ -0,0 +1,6 @@ +use vergen::{vergen, Config}; + +fn main() { + // Generate the default 'cargo:' instruction output + vergen(Config::default()).unwrap() +} diff --git a/bot/src/handler.rs b/bot/src/handler.rs new file mode 100644 index 0000000..ae987b5 --- /dev/null +++ b/bot/src/handler.rs @@ -0,0 +1,413 @@ +use std::{borrow::Cow, collections::HashSet}; + +use eh2telegraph::{ + collector::{e_hentai::EHCollector, exhentai::EXCollector, nhentai::NHCollector}, + searcher::{ + f_hash::FHashConvertor, + saucenao::{SaucenaoOutput, SaucenaoParsed, SaucenaoSearcher}, + ImageSearcher, + }, + storage::KVStorage, + sync::Synchronizer, +}; + +use reqwest::Url; +use teloxide::{ + adaptors::DefaultParseMode, + prelude2::*, + utils::{ + command::BotCommand, + markdown::{code_inline, escape, link}, + }, +}; +use tracing::{info, trace}; + +use crate::{ok_or_break, util::PrettyChat}; + +const MIN_SIMILARITY: u8 = 70; +const MIN_SIMILARITY_PRIVATE: u8 = 50; + +#[derive(BotCommand, Clone)] +#[command( + rename = "lowercase", + description = "\ + This is a gallery synchronization robot that is convenient for users to view pictures directly in Telegram.\n\ + 这是一个方便用户直接在 Telegram 里看图的画廊同步机器人。\n\ + Join develop group or contact @ByteRabbit if you need.\n\ + 如有问题请在开发群里反馈或私聊 @ByteRabbit。\n\n\ + Bot supports sync with command, text url, or image(private chat search thrashold is lower).\n\ + 机器人支持通过 命令、直接发送链接、图片(私聊搜索相似度阈值会更低) 的形式同步。\n\n\ + Bot develop group / Bot 开发群 https://t.me/TGSyncBotWorkGroup\n\ + And welcome to join our channel / 作者的频道 https://t.me/sesecollection\n\n\ + These commands are supported:\n\ + 目前支持这些指令:" +)] +pub enum Command { + #[command(description = "Display this help. 显示这条帮助信息。")] + Help, + #[command(description = "Show bot verison. 显示机器人版本。")] + Version, + #[command(description = "Show your account id. 显示你的账号 ID。")] + Id, + #[command( + description = "Sync a gallery(e-hentai/exhentai/nhentai are supported now). 同步一个画廊(目前支持 EH/EX/NH)" + )] + Sync(String), +} + +#[derive(BotCommand, Clone)] +#[command(rename = "lowercase", description = "Command for admins")] +pub enum AdminCommand { + #[command(description = "Delete cache with given key.")] + Delete(String), +} + +pub struct Handler { + pub synchronizer: Synchronizer, + pub searcher: SaucenaoSearcher, + pub convertor: FHashConvertor, + pub admins: HashSet, + + single_flight: singleflight_async::SingleFlight, +} + +impl Handler +where + C: KVStorage + Send + Sync + 'static, +{ + pub fn new(synchronizer: Synchronizer, admins: HashSet) -> Self { + Self { + synchronizer, + searcher: SaucenaoSearcher::new_from_config(), + convertor: FHashConvertor::new_from_config(), + admins, + + single_flight: Default::default(), + } + } + + /// Executed when a command comes in and parsed successfully. + pub async fn respond_cmd( + &'static self, + bot: AutoSend>, + msg: Message, + command: Command, + ) -> ControlFlow<()> { + match command { + Command::Help => { + let _ = bot + .send_message(msg.chat.id, escape(&Command::descriptions())) + .reply_to_message_id(msg.id) + .await; + } + Command::Version => { + let _ = bot + .send_message(msg.chat.id, escape(crate::version::VERSION)) + .reply_to_message_id(msg.id) + .await; + } + Command::Id => { + let _ = bot + .send_message( + msg.chat.id, + format!( + "Current chat id is {} \\(in private chat this is your account id\\)", + code_inline(&msg.chat.id.to_string()) + ), + ) + .reply_to_message_id(msg.id) + .await; + } + Command::Sync(url) => { + if url.is_empty() { + let _ = bot + .send_message(msg.chat.id, escape("Usage: /sync url")) + .reply_to_message_id(msg.id) + .await; + return ControlFlow::BREAK; + } + + info!( + "[cmd handler] receive sync request from {:?} for {url}", + PrettyChat(&msg.chat) + ); + let msg: Message = ok_or_break!( + bot.send_message(msg.chat.id, escape(&format!("Syncing url {url}"))) + .reply_to_message_id(msg.id) + .await + ); + tokio::spawn(async move { + let _ = bot + .edit_message_text(msg.chat.id, msg.id, self.sync_response(&url).await) + .await; + }); + } + }; + + ControlFlow::BREAK + } + + pub async fn respond_admin_cmd( + &'static self, + bot: AutoSend>, + msg: Message, + command: AdminCommand, + ) -> ControlFlow<()> { + match command { + AdminCommand::Delete(key) => { + let _ = self.synchronizer.delete_cache(&key).await; + let _ = bot + .send_message(msg.chat.id, escape(&format!("Key {key} deleted."))) + .reply_to_message_id(msg.id) + .await; + ControlFlow::BREAK + } + } + } + + pub async fn respond_text( + &'static self, + bot: AutoSend>, + msg: Message, + ) -> ControlFlow<()> { + let maybe_link = { + let entries = msg + .entities() + .map(|es| { + es.iter().filter_map(|e| { + if let teloxide::types::MessageEntityKind::TextLink { url } = &e.kind { + Synchronizer::match_url_from_text(url.as_ref()).map(ToOwned::to_owned) + } else { + None + } + }) + }) + .into_iter() + .flatten(); + msg.text() + .and_then(|content| { + Synchronizer::match_url_from_text(content).map(ToOwned::to_owned) + }) + .into_iter() + .chain(entries) + .next() + }; + + if let Some(url) = maybe_link { + info!( + "[text handler] receive sync request from {:?} for {url}", + PrettyChat(&msg.chat) + ); + let msg: Message = ok_or_break!( + bot.send_message(msg.chat.id, escape(&format!("Syncing url {url}"))) + .reply_to_message_id(msg.id) + .await + ); + tokio::spawn(async move { + let _ = bot + .edit_message_text(msg.chat.id, msg.id, self.sync_response(&url).await) + .await; + }); + return ControlFlow::BREAK; + } + + // fallback to the next branch + ControlFlow::CONTINUE + } + + pub async fn respond_caption( + &'static self, + bot: AutoSend>, + msg: Message, + ) -> ControlFlow<()> { + let caption_entities = msg.caption_entities(); + let mut final_url = None; + for entry in caption_entities.map(|x| x.iter()).into_iter().flatten() { + let url = match &entry.kind { + teloxide::types::MessageEntityKind::Url => { + let raw = msg + .caption() + .expect("Url MessageEntry found but caption is None"); + let encoded: Vec<_> = raw + .encode_utf16() + .into_iter() + .skip(entry.offset) + .take(entry.length) + .collect(); + let content = ok_or_break!(String::from_utf16(&encoded)); + Cow::from(content) + } + teloxide::types::MessageEntityKind::TextLink { url } => Cow::from(url.as_ref()), + _ => { + continue; + } + }; + let url = if let Some(c) = Synchronizer::match_url_from_url(&url) { + c + } else { + continue; + }; + final_url = Some(url.to_string()); + break; + } + + match final_url { + Some(url) => { + info!( + "[caption handler] receive sync request from {:?} for {url}", + PrettyChat(&msg.chat) + ); + let msg: Message = ok_or_break!( + bot.send_message(msg.chat.id, escape(&format!("Syncing url {url}"))) + .reply_to_message_id(msg.id) + .await + ); + let url = url.to_string(); + tokio::spawn(async move { + let _ = bot + .edit_message_text(msg.chat.id, msg.id, self.sync_response(&url).await) + .await; + }); + ControlFlow::BREAK + } + None => ControlFlow::CONTINUE, + } + } + + pub async fn respond_photo( + &'static self, + bot: AutoSend>, + msg: Message, + ) -> ControlFlow<()> { + let first_photo = match msg.photo().and_then(|x| x.first()) { + Some(p) => p, + None => { + return ControlFlow::CONTINUE; + } + }; + + let f = ok_or_break!(bot.get_file(&first_photo.file_id).await); + let mut buf: Vec = Vec::with_capacity(f.file_size as usize); + ok_or_break!(teloxide::net::Download::download_file(&bot, &f.file_path, &mut buf).await); + let search_result: SaucenaoOutput = ok_or_break!(self.searcher.search(buf).await); + + let mut url_sim = None; + let threshold = if msg.chat.is_private() { + MIN_SIMILARITY_PRIVATE + } else { + MIN_SIMILARITY + }; + for element in search_result + .data + .into_iter() + .filter(|x| x.similarity >= threshold) + { + match element.parsed { + SaucenaoParsed::EHentai(f_hash) => { + url_sim = Some(( + ok_or_break!(self.convertor.convert_to_gallery(&f_hash).await), + element.similarity, + )); + break; + } + SaucenaoParsed::NHentai(nid) => { + url_sim = Some((format!("https://nhentai.net/g/{nid}/"), element.similarity)); + break; + } + _ => continue, + } + } + + let (url, sim) = match url_sim { + Some(u) => u, + None => { + trace!("[photo handler] image not found"); + return ControlFlow::CONTINUE; + } + }; + + info!( + "[photo handler] receive sync request from {:?} for {url} with similarity {sim}", + PrettyChat(&msg.chat) + ); + + if let Ok(msg) = bot + .send_message(msg.chat.id, escape(&format!("Syncing url {url}"))) + .reply_to_message_id(msg.id) + .await + { + tokio::spawn(async move { + let _ = bot + .edit_message_text(msg.chat.id, msg.id, self.sync_response(&url).await) + .await; + }); + } + + ControlFlow::BREAK + } + + pub async fn respond_default( + &'static self, + bot: AutoSend>, + msg: Message, + ) -> ControlFlow<()> { + if msg.chat.is_private() { + ok_or_break!( + bot.send_message(msg.chat.id, escape("Unrecognized message.")) + .reply_to_message_id(msg.id) + .await + ); + } + #[cfg(debug_assertions)] + tracing::warn!("{:?}", msg); + ControlFlow::BREAK + } + + async fn sync_response(&self, url: &str) -> String { + self.single_flight + .work(url, || async { + match self.route_sync(url).await { + Ok(url) => { + format!("Sync to telegraph finished: {}", link(&url, &escape(&url))) + } + Err(e) => { + format!("Sync to telegraph failed: {}", escape(&e.to_string())) + } + } + }) + .await + } + + async fn route_sync(&self, url: &str) -> anyhow::Result { + let u = Url::parse(url).map_err(|_| anyhow::anyhow!("Invalid url"))?; + let host = u.host_str().unwrap_or_default(); + let path = u.path().to_string(); + + // TODO: use macro to generate them + #[allow(clippy::single_match)] + match host { + "e-hentai.org" => { + info!("[registry] sync e-hentai for path {}", path); + self.synchronizer + .sync::(path) + .await + .map_err(anyhow::Error::from) + } + "nhentai.to" | "nhentai.net" => { + info!("[registry] sync nhentai for path {}", path); + self.synchronizer + .sync::(path) + .await + .map_err(anyhow::Error::from) + } + "exhentai.org" => { + info!("[registry] sync exhentai for path {}", path); + self.synchronizer + .sync::(path) + .await + .map_err(anyhow::Error::from) + } + _ => Err(anyhow::anyhow!("no matching collector")), + } + } +} diff --git a/bot/src/main.rs b/bot/src/main.rs new file mode 100644 index 0000000..3a07526 --- /dev/null +++ b/bot/src/main.rs @@ -0,0 +1,216 @@ +#![feature(control_flow_enum)] + +use eh2telegraph::{ + collector::Registry, + config::{self}, + http_proxy::ProxiedClient, + storage, + sync::Synchronizer, + telegraph::Telegraph, +}; + +use clap::Parser; + +use teloxide::{ + adaptors::DefaultParseMode, + dispatching::update_listeners, + error_handlers::IgnoringErrorHandler, + prelude2::*, + types::{AllowedUpdate, ChatPermissions, ParseMode, UpdateKind}, +}; + +use handler::{Command, Handler}; + +use crate::{ + handler::AdminCommand, + util::{wrap_endpoint, PrettyChat}, +}; + +mod handler; +mod util; +mod version; + +#[derive(Debug, serde::Deserialize)] +pub struct BaseConfig { + pub bot_token: String, + pub telegraph: TelegraphConfig, + #[serde(default)] + pub admins: Vec, +} + +#[derive(Debug, serde::Deserialize)] +pub struct TelegraphConfig { + pub tokens: Vec, + pub author_name: Option, + pub author_url: Option, +} + +#[derive(Parser, Debug)] +#[clap(author, version=version::VERSION, about, long_about = "eh2telegraph sync bot")] +struct Args { + #[clap(short, long, help = "Config file path")] + config: Option, +} + +#[tokio::main] +async fn main() { + let args = Args::parse(); + + let timer = tracing_subscriber::fmt::time::LocalTime::new(time::macros::format_description!( + "[month]-[day] [hour]:[minute]:[second]" + )); + tracing_subscriber::fmt().with_timer(timer).init(); + tracing::info!("initializing..."); + + config::init(args.config); + let base_config: BaseConfig = config::parse("base") + .expect("unable to parse base config") + .expect("base config can not be empty"); + let telegraph_config = base_config.telegraph; + let telegraph = + Telegraph::new(telegraph_config.tokens).with_proxy(ProxiedClient::new_from_config()); + + let registry = Registry::new_from_config(); + #[cfg(debug_assertions)] + let cache = storage::SimpleMemStorage::default(); + #[cfg(not(debug_assertions))] + let cache = + storage::cloudflare_kv::CFStorage::new_from_config().expect("unable to build storage"); + let mut synchronizer = Synchronizer::new(telegraph, registry, cache); + if telegraph_config.author_name.is_some() { + synchronizer = + synchronizer.with_author(telegraph_config.author_name, telegraph_config.author_url); + } + + let admins = base_config.admins.into_iter().collect(); + let handler = Box::leak(Box::new(Handler::new(synchronizer, admins))) as &Handler<_>; + + // === Bot related === + let command_handler = move |bot: AutoSend>, + message: Message, + command: Command| async move { + handler.respond_cmd(bot, message, command).await + }; + let admin_command_handler = move |bot: AutoSend>, + message: Message, + command: AdminCommand| async move { + handler.respond_admin_cmd(bot, message, command).await + }; + let text_handler = move |bot: AutoSend>, message: Message| async move { + handler.respond_text(bot, message).await + }; + let caption_handler = move |bot: AutoSend>, message: Message| async move { + handler.respond_caption(bot, message).await + }; + let photo_handler = move |bot: AutoSend>, message: Message| async move { + handler.respond_photo(bot, message).await + }; + let default_handler = move |bot: AutoSend>, message: Message| async move { + handler.respond_default(bot, message).await + }; + let permission_filter = |bot: AutoSend>, message: Message| async move { + // If the bot is blocked, we will leave chat and not respond. + let blocked = message + .chat + .permissions() + .map(|p| !p.contains(ChatPermissions::SEND_MESSAGES)) + .unwrap_or_default(); + if blocked { + tracing::info!( + "[permission filter] leave chat {:?}", + PrettyChat(&message.chat) + ); + let _ = bot.leave_chat(message.chat.id).await; + None + } else { + Some(message) + } + }; + + let bot = Bot::new(base_config.bot_token) + .parse_mode(ParseMode::MarkdownV2) + .auto_send(); + let mut bot_dispatcher = Dispatcher::builder( + bot.clone(), + dptree::entry() + .chain(dptree::filter_map(move |update: Update| { + match update.kind { + UpdateKind::Message(x) | UpdateKind::EditedMessage(x) => Some(x), + _ => None, + } + })) + .chain(dptree::filter_map_async(permission_filter)) + .branch( + dptree::entry() + .chain(dptree::filter(move |message: Message| { + handler.admins.contains(&message.chat.id) + })) + .filter_command::() + .branch(wrap_endpoint(admin_command_handler)), + ) + .branch( + dptree::entry() + .filter_command::() + .branch(wrap_endpoint(command_handler)), + ) + .branch( + dptree::entry() + .chain(dptree::filter_map(move |message: Message| { + // Ownership mechanism does not allow using map. + #[allow(clippy::manual_map)] + match message.text() { + Some(v) if !v.is_empty() => Some(message), + _ => None, + } + })) + .branch(wrap_endpoint(text_handler)), + ) + .branch( + dptree::entry() + .chain(dptree::filter_map(move |message: Message| { + // Ownership mechanism does not allow using map. + #[allow(clippy::manual_map)] + match message.caption_entities() { + Some(v) if !v.is_empty() => Some(message), + _ => None, + } + })) + .branch(wrap_endpoint(caption_handler)), + ) + .branch( + dptree::entry() + .chain(dptree::filter_map(move |message: Message| { + // Ownership mechanism does not allow using map. + #[allow(clippy::manual_map)] + match message.photo() { + Some(v) if !v.is_empty() => Some(message), + _ => None, + } + })) + .branch(wrap_endpoint(photo_handler)), + ) + .branch(wrap_endpoint(default_handler)), + ) + .default_handler(Box::new(|_upd| { + #[cfg(debug_assertions)] + tracing::warn!("Unhandled update: {:?}", _upd); + Box::pin(async {}) + })) + .error_handler(std::sync::Arc::new(IgnoringErrorHandler)) + .build(); + bot_dispatcher.setup_ctrlc_handler(); + let bot_listener = update_listeners::polling( + bot, + Some(std::time::Duration::from_secs(10)), + None, + Some(vec![AllowedUpdate::Message]), + ); + + tracing::info!("initializing finished, bot is running"); + bot_dispatcher + .dispatch_with_listener( + bot_listener, + LoggingErrorHandler::with_custom_text("An error from the update listener"), + ) + .await; +} diff --git a/bot/src/util.rs b/bot/src/util.rs new file mode 100644 index 0000000..edeaa9c --- /dev/null +++ b/bot/src/util.rs @@ -0,0 +1,69 @@ +use std::{convert::Infallible, ops::ControlFlow, sync::Arc}; + +use dptree::{di::Injectable, from_fn, Handler}; + +pub struct PrettyChat<'a>(pub &'a teloxide::types::Chat); + +impl<'a> std::fmt::Debug for PrettyChat<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.0.is_group() || self.0.is_supergroup() { + write!(f, "GroupChat")?; + self.0.title().map(|x| write!(f, " title: {}", x)); + self.0 + .description() + .map(|x| write!(f, " description: {}", x)); + } else if self.0.is_private() { + write!(f, "PrivateChat")?; + self.0.username().map(|x| write!(f, " username: @{}", x)); + self.0.first_name().map(|x| write!(f, " first_name: {}", x)); + self.0.last_name().map(|x| write!(f, " last_name: {}", x)); + self.0.bio().map(|x| write!(f, " bio: {}", x)); + } else if self.0.is_channel() { + write!(f, "Channel")?; + self.0.username().map(|x| write!(f, " username: @{}", x)); + self.0.title().map(|x| write!(f, " title: {}", x)); + self.0 + .description() + .map(|x| write!(f, ", description: {}", x)); + } + Ok(()) + } +} + +pub fn wrap_endpoint<'a, F, Input, Output, FnArgs>( + f: F, +) -> Handler<'a, Input, Result, Infallible> +where + F: Injectable, FnArgs> + Send + Sync + 'a, + Input: Send + Sync + 'a, + Output: Send + Sync + 'a, +{ + let f = Arc::new(f); + + from_fn(move |event, _cont| { + let f = Arc::clone(&f); + + async move { + let f = f.inject(&event); + let cf = f().await; + drop(f); + + match cf { + ControlFlow::Continue(_) => ControlFlow::Continue(event), + ControlFlow::Break(out) => ControlFlow::Break(Ok(out)), + } + } + }) +} + +#[macro_export] +macro_rules! ok_or_break { + ($e: expr) => { + match $e { + Ok(r) => r, + Err(_) => { + return ControlFlow::BREAK; + } + } + }; +} diff --git a/bot/src/version.rs b/bot/src/version.rs new file mode 100644 index 0000000..47992e3 --- /dev/null +++ b/bot/src/version.rs @@ -0,0 +1,18 @@ +pub(crate) static VERSION: &str = concat!( + "\n", + "Build Timestamp: \t", + env!("VERGEN_BUILD_TIMESTAMP"), + "\n", + "Package Version: \t", + env!("VERGEN_BUILD_SEMVER"), + "\n", + "rustc Version: \t\t", + env!("VERGEN_RUSTC_SEMVER"), + "\n", + "cargo Profile: \t\t", + env!("VERGEN_CARGO_PROFILE"), + "\n", + "cargo Target: \t\t", + env!("VERGEN_CARGO_TARGET_TRIPLE"), + "\n", +); diff --git a/config_example.yaml b/config_example.yaml new file mode 100644 index 0000000..11bbbee --- /dev/null +++ b/config_example.yaml @@ -0,0 +1,27 @@ +base: + bot_token: xxx:xxxx + admins: + - 0 + telegraph: + tokens: + - xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + author_name: Test Name + author_url: https://github.com/qini7-sese/eh2telegraph + +proxy: + endpoint: https://proxy.xxx.workers.dev/ + authorization: xxx + +http: + ipv6_prefix: + +exhentai: + ipb_pass_hash: xxx + ipb_member_id: xxx + igneous: xxx + +worker_kv: + endpoint: https://kv.xxx.workers.dev + token: xxx + cache_size: 10240 + expire_sec: 5184000 # 60 days diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b5f79ed --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: "3" +services: + ehbot: + image: ghcr.io/qini7-sese/ehbot:latest + container_name: ehbot + restart: always + network_mode: "host" + environment: + CONFIG_FILE: "/config.yaml" + TZ: Asia/Shanghai + volumes: + - "./config.yaml:/config.yaml:ro" + logging: + driver: journald \ No newline at end of file diff --git a/eh2telegraph/Cargo.toml b/eh2telegraph/Cargo.toml new file mode 100644 index 0000000..d4498eb --- /dev/null +++ b/eh2telegraph/Cargo.toml @@ -0,0 +1,30 @@ +[package] +edition = "2021" +name = "eh2telegraph" +version = "0.1.0" + +[dependencies] +again = {version = "0.1", default_features = false, features = ["rand"]} +anyhow = "1" +bytes = "1" +clap = "3" +cloudflare-kv-proxy = "0.1" +derive_more = {version = "0.99", features = ["from_str"]} +futures = "0.3" +hashlink = "0.8" +ipnet = "2" +lazy_static = "1" +once_cell = "1" +parking_lot = {version = "0.12", features = ["hardware-lock-elision"]} +rand = "0.8" +regex = "1" +reqwest = {version = "0.11", default-features = false, features = ["json", "multipart", "rustls-tls"]} +rustls = {version = "0.20", features = ["dangerous_configuration"]} +serde = {version = "1", features = ["derive"]} +serde_with = {version = "1", features = ["macros", "json"]} +serde_yaml = "0.8" +thiserror = "1" +tokio = {version = "1", default-features = false, features = ["rt-multi-thread", "macros", "net", "sync", "time", "parking_lot"]} +tracing = "0.1" +webpki = "0.22" +webpki-roots = "0.22" diff --git a/eh2telegraph/src/buffer.rs b/eh2telegraph/src/buffer.rs new file mode 100644 index 0000000..00bf284 --- /dev/null +++ b/eh2telegraph/src/buffer.rs @@ -0,0 +1,95 @@ +/// ImageBuffer for upload in batch. +pub struct ImageBuffer { + buf: Vec, + size: usize, +} + +impl Default for ImageBuffer { + #[inline] + fn default() -> Self { + Self { + buf: Vec::new(), + size: 0, + } + } +} + +impl ImageBuffer +where + T: DataSized, +{ + #[inline] + pub fn new() -> Self { + Self::default() + } + + #[inline] + pub fn with_capacity(n: usize) -> Self { + Self { + buf: Vec::with_capacity(n), + size: 0, + } + } + + #[inline] + pub fn push(&mut self, data: T) { + self.size += data.size(); + self.buf.push(data); + } + + #[inline] + pub fn swap(&mut self) -> (Vec, usize) { + let mut out = Vec::with_capacity(self.buf.len() * 2); + std::mem::swap(&mut self.buf, &mut out); + + let mut size = 0; + std::mem::swap(&mut self.size, &mut size); + (out, size) + } + + #[inline] + pub fn len(&self) -> usize { + self.buf.len() + } + + #[inline] + pub fn is_empty(&self) -> bool { + self.buf.len() == 0 + } + + #[inline] + pub fn size(&self) -> usize { + self.size + } + + #[inline] + pub fn clear(&mut self) { + self.size = 0; + self.buf.clear(); + } +} + +pub trait DataSized { + fn size(&self) -> usize; +} + +impl DataSized for bytes::Bytes { + #[inline] + fn size(&self) -> usize { + self.len() + } +} + +impl DataSized for Vec { + #[inline] + fn size(&self) -> usize { + self.len() + } +} + +impl DataSized for Box<[u8; N]> { + #[inline] + fn size(&self) -> usize { + N + } +} diff --git a/eh2telegraph/src/collector/e_hentai.rs b/eh2telegraph/src/collector/e_hentai.rs new file mode 100644 index 0000000..23b15c6 --- /dev/null +++ b/eh2telegraph/src/collector/e_hentai.rs @@ -0,0 +1,254 @@ +/// nhentai collector. +/// Host matching: e-hentai.org +use crate::{ + http_client::{GhostClient, GhostClientBuilder, UA}, + stream::AsyncStream, + util::match_first_group, + util::{get_bytes, get_string}, +}; +use again::RetryPolicy; +use ipnet::Ipv6Net; +use regex::Regex; +use reqwest::header; + +use std::time::Duration; + +use super::{ + utils::paged::{PageFormatter, PageIndicator, Paged}, + AlbumMeta, Collector, ImageData, ImageMeta, +}; + +lazy_static::lazy_static! { + static ref PAGE_RE: Regex = Regex::new(r#""#).unwrap(); + static ref IMG_RE: Regex = Regex::new(r#"(.*?)"#).unwrap(); + + static ref RETRY_POLICY: RetryPolicy = RetryPolicy::fixed(Duration::from_millis(200)) + .with_max_retries(5) + .with_jitter(true); +} + +#[derive(Debug, Clone, Default)] +pub struct EHCollector { + client: GhostClient, + raw_client: reqwest::Client, +} + +impl EHCollector { + pub fn new(prefix: Option) -> Self { + let mut request_headers = header::HeaderMap::new(); + request_headers.insert( + header::COOKIE, + header::HeaderValue::from_str("nw=1").unwrap(), + ); + + Self { + client: GhostClientBuilder::default() + .with_default_headers(request_headers) + .with_cf_resolve(&["e-hentai.org"]) + .build(prefix), + raw_client: reqwest::Client::builder().user_agent(UA).build().unwrap(), + } + } + + pub fn new_from_config() -> anyhow::Result { + let mut request_headers = header::HeaderMap::new(); + request_headers.insert( + header::COOKIE, + header::HeaderValue::from_str("nw=1").unwrap(), + ); + + Ok(Self { + client: GhostClientBuilder::default() + .with_default_headers(request_headers) + .with_cf_resolve(&["e-hentai.org"]) + .build_from_config()?, + raw_client: reqwest::Client::builder().user_agent(UA).build().unwrap(), + }) + } +} + +impl Collector for EHCollector { + type FetchError = anyhow::Error; + type FetchFuture<'a> = + impl std::future::Future>; + + type StreamError = anyhow::Error; + type ImageStream = EHImageStream; + + #[inline] + fn name() -> &'static str { + "e-hentai" + } + + fn fetch(&self, path: String) -> Self::FetchFuture<'_> { + async move { + // normalize url + let mut parts = path.trim_matches(|c| c == '/').split('/'); + let g = parts.next(); + let album_id = parts.next(); + let album_token = parts.next(); + let (album_id, album_token) = match (g, album_id, album_token) { + (Some("g"), Some(album_id), Some(album_token)) => (album_id, album_token), + _ => { + return Err(anyhow::anyhow!("invalid input path({path}), gallery url is expected(like https://e-hentai.org/g/2127986/da1deffea5)")); + } + }; + let url = format!("https://e-hentai.org/g/{album_id}/{album_token}"); + tracing::info!("[e-hentai] process {url}"); + + // clone client to force changing ip + let client = self.client.clone(); + let mut paged = Paged::new(0, EHPageIndicator { base: url.clone() }); + let gallery_pages = paged.pages(&client).await?; + + // Since paged returns at least one page, we can safely get it. + let title = match_first_group(&TITLE_RE, &gallery_pages[0]) + .unwrap_or("No Title") + .to_string(); + + let mut image_page_links = Vec::new(); + for gallery_page in gallery_pages.iter() { + PAGE_RE.captures_iter(gallery_page).for_each(|c| { + let matching = c.get(1).expect("regexp is matched but no group 1 found"); + image_page_links.push(matching.as_str().to_string()); + }); + } + + if image_page_links.is_empty() { + return Err(anyhow::anyhow!( + "invalid url, maybe resource has been deleted." + )); + } + + Ok(( + AlbumMeta { + link: url, + name: title, + class: None, + description: None, + authors: None, + tags: None, + }, + EHImageStream { + client, + raw_client: self.raw_client.clone(), + image_page_links: image_page_links.into_iter(), + }, + )) + } + } +} + +#[derive(Debug)] +pub struct EHImageStream { + client: GhostClient, + raw_client: reqwest::Client, + image_page_links: std::vec::IntoIter, +} + +impl EHImageStream { + async fn load_image( + client: &GhostClient, + raw_client: &reqwest::Client, + link: String, + ) -> anyhow::Result<(ImageMeta, ImageData)> { + let content = RETRY_POLICY + .retry(|| async { get_string(client, &link).await }) + .await?; + let img_url = match_first_group(&IMG_RE, &content) + .ok_or_else(|| anyhow::anyhow!("unable to find image in page"))?; + let image_data = RETRY_POLICY + .retry(|| async { get_bytes(raw_client, img_url).await }) + .await?; + + tracing::trace!( + "download e-hentai image with size {}, link: {link}", + image_data.len() + ); + let meta = ImageMeta { + id: link, + url: img_url.to_string(), + description: None, + }; + Ok((meta, image_data)) + } +} + +impl AsyncStream for EHImageStream { + type Item = anyhow::Result<(ImageMeta, ImageData)>; + + type Future = impl std::future::Future; + + fn next(&mut self) -> Option { + let link = self.image_page_links.next()?; + let client = self.client.clone(); + let raw_client = self.raw_client.clone(); + Some(async move { Self::load_image(&client, &raw_client, link).await }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.image_page_links.size_hint() + } +} + +struct EHPageIndicator { + base: String, +} + +impl PageFormatter for EHPageIndicator { + fn format_n(&self, n: usize) -> String { + format!("{}/?p={}", self.base, n) + } +} + +impl PageIndicator for EHPageIndicator { + fn is_last_page(&self, content: &str, next_page: usize) -> bool { + let html = format!( + "", + self.base, next_page + ); + !content.contains(&html) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[tokio::test] + async fn demo() { + let collector = EHCollector { + raw_client: Default::default(), + client: Default::default(), + }; + let (album, mut image_stream) = collector + .fetch("/g/2122174/fd2525031e".to_string()) + .await + .unwrap(); + println!("album: {:?}", album); + + let maybe_first_image = image_stream.next().unwrap().await; + if let Ok((meta, data)) = maybe_first_image { + println!("first image meta: {meta:?}"); + println!("first image data length: {}", data.len()); + } + } + + #[ignore] + #[test] + fn regex_match() { + // test page: https://e-hentai.org/g/2122174/fd2525031e + let r = Regex::new(r#""#).unwrap(); + let h = r#"
007
008"#; + + let mut iter = r.captures_iter(h); + let first = iter.next().unwrap(); + println!("{}", first.get(1).unwrap().as_str()); + + let second = iter.next().unwrap(); + println!("{}", second.get(1).unwrap().as_str()); + } +} diff --git a/eh2telegraph/src/collector/exhentai.rs b/eh2telegraph/src/collector/exhentai.rs new file mode 100644 index 0000000..575aaae --- /dev/null +++ b/eh2telegraph/src/collector/exhentai.rs @@ -0,0 +1,282 @@ +use std::time::Duration; + +use again::RetryPolicy; +use regex::Regex; +use reqwest::header; +use serde::Deserialize; + +use crate::{ + config, + http_client::UA, + http_proxy::ProxiedClient, + stream::AsyncStream, + util::match_first_group, + util::{get_bytes, get_string}, +}; + +use super::{ + utils::paged::{PageFormatter, PageIndicator, Paged}, + AlbumMeta, Collector, ImageData, ImageMeta, +}; + +lazy_static::lazy_static! { + static ref PAGE_RE: Regex = Regex::new(r#""#).unwrap(); + static ref IMG_RE: Regex = Regex::new(r#"(.*?)"#).unwrap(); + + static ref RETRY_POLICY: RetryPolicy = RetryPolicy::fixed(Duration::from_millis(200)) + .with_max_retries(5) + .with_jitter(true); +} +const CONFIG_KEY: &str = "exhentai"; + +#[derive(Debug, Clone)] +pub struct EXCollector { + proxy_client: ProxiedClient, + client: reqwest::Client, +} + +#[derive(Debug, Deserialize)] +pub struct ExConfig { + pub ipb_pass_hash: String, + pub ipb_member_id: String, + pub igneous: String, +} + +impl EXCollector { + pub fn new(config: &ExConfig, proxy_client: ProxiedClient) -> anyhow::Result { + let cookie_value = format!( + "ipb_pass_hash={};ipb_member_id={};igneous={};nw=1", + config.ipb_pass_hash, config.ipb_member_id, config.igneous + ); + + // set headers with exhentai cookies + let mut request_headers = header::HeaderMap::new(); + request_headers.insert( + header::COOKIE, + header::HeaderValue::from_str(&cookie_value)?, + ); + Ok(Self { + client: { + reqwest::Client::builder() + .user_agent(UA) + .default_headers(request_headers.clone()) + .build() + .expect("build reqwest client failed") + }, + proxy_client: proxy_client.with_default_headers(request_headers), + }) + } + + pub fn new_from_config() -> anyhow::Result { + let config: ExConfig = config::parse(CONFIG_KEY)? + .ok_or_else(|| anyhow::anyhow!("exhentai config(key: exhentai) not found"))?; + let proxy_client = ProxiedClient::new_from_config(); + Self::new(&config, proxy_client) + } + + pub fn get_client(&self) -> reqwest::Client { + self.client.clone() + } +} + +impl Collector for EXCollector { + type FetchError = anyhow::Error; + type FetchFuture<'a> = + impl std::future::Future>; + + type StreamError = anyhow::Error; + type ImageStream = EXImageStream; + + #[inline] + fn name() -> &'static str { + "exhentai" + } + + fn fetch(&self, path: String) -> Self::FetchFuture<'_> { + async move { + // normalize url + let mut parts = path.trim_matches(|c| c == '/').split('/'); + let g = parts.next(); + let album_id = parts.next(); + let album_token = parts.next(); + let (album_id, album_token) = match (g, album_id, album_token) { + (Some("g"), Some(album_id), Some(album_token)) => (album_id, album_token), + _ => { + return Err(anyhow::anyhow!("invalid input path({path}), gallery url is expected(like https://exhentai.org/g/2129939/01a6e086b9)")); + } + }; + let url = format!("https://exhentai.org/g/{album_id}/{album_token}"); + tracing::info!("[exhentai] process {url}"); + + let mut paged = Paged::new(0, EXPageIndicator { base: url.clone() }); + let gallery_pages = paged.pages(&self.proxy_client).await?; + + // Since paged returns at least one page, we can safely get it. + let title = match_first_group(&TITLE_RE, &gallery_pages[0]) + .unwrap_or("No Title") + .to_string(); + + let mut image_page_links = Vec::new(); + for gallery_page in gallery_pages.iter() { + PAGE_RE.captures_iter(gallery_page).for_each(|c| { + let matching = c.get(1).expect("regexp is matched but no group 1 found"); + image_page_links.push(matching.as_str().to_string()); + }); + } + + if image_page_links.is_empty() { + return Err(anyhow::anyhow!( + "invalid url, maybe resource has been deleted, or our ip is blocked." + )); + } + + Ok(( + AlbumMeta { + link: url, + name: title, + class: None, + description: None, + authors: None, + tags: None, + }, + EXImageStream { + client: self.client.clone(), + proxy_client: self.proxy_client.clone(), + image_page_links: image_page_links.into_iter(), + }, + )) + } + } +} + +#[derive(Debug)] +pub struct EXImageStream { + client: reqwest::Client, + proxy_client: ProxiedClient, + image_page_links: std::vec::IntoIter, +} + +impl EXImageStream { + async fn load_image( + proxy_client: ProxiedClient, + client: reqwest::Client, + link: String, + ) -> anyhow::Result<(ImageMeta, ImageData)> { + let content = RETRY_POLICY + .retry(|| async { get_string(&proxy_client, &link).await }) + .await?; + let img_url = match_first_group(&IMG_RE, &content) + .ok_or_else(|| anyhow::anyhow!("unable to find image in page"))?; + let image_data = RETRY_POLICY + .retry(|| async { get_bytes(&client, img_url).await }) + .await?; + + tracing::trace!( + "download exhentai image with size {}, link: {link}", + image_data.len() + ); + let meta = ImageMeta { + id: link, + url: img_url.to_string(), + description: None, + }; + Ok((meta, image_data)) + } +} + +impl AsyncStream for EXImageStream { + type Item = anyhow::Result<(ImageMeta, ImageData)>; + + type Future = impl std::future::Future; + + fn next(&mut self) -> Option { + let link = self.image_page_links.next()?; + let client = self.client.clone(); + let proxy_client = self.proxy_client.clone(); + Some(async move { Self::load_image(proxy_client, client, link).await }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.image_page_links.size_hint() + } +} + +struct EXPageIndicator { + base: String, +} + +impl PageFormatter for EXPageIndicator { + fn format_n(&self, n: usize) -> String { + format!("{}/?p={}", self.base, n) + } +} + +impl PageIndicator for EXPageIndicator { + fn is_last_page(&self, content: &str, next_page: usize) -> bool { + let html = format!( + "", + self.base, next_page + ); + !content.contains(&html) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[ignore] + #[tokio::test] + async fn demo() { + let config = ExConfig { + ipb_pass_hash: "balabala".to_string(), + ipb_member_id: "balabala".to_string(), + igneous: "balabala".to_string(), + }; + println!("config {:#?}", config); + let collector = EXCollector::new(&config, ProxiedClient::default()).unwrap(); + let (album, mut image_stream) = collector + .fetch("/g/2129939/01a6e086b9".to_string()) + .await + .unwrap(); + println!("album: {:?}", album); + + let maybe_first_image = image_stream.next().unwrap().await; + if let Ok((meta, data)) = maybe_first_image { + println!("first image meta: {meta:?}"); + println!("first image data length: {}", data.len()); + } + } + + #[ignore] + #[tokio::test] + async fn invalid_url() { + let config = ExConfig { + ipb_pass_hash: "balabala".to_string(), + ipb_member_id: "balabala".to_string(), + igneous: "balabala".to_string(), + }; + println!("config {:#?}", config); + let collector = EXCollector::new(&config, ProxiedClient::default()).unwrap(); + let output = collector.fetch("/g/2129939/00000".to_string()).await; + assert!(output.is_err()); + println!("output err {:?}", output); + } + + #[ignore] + #[test] + fn regex_match() { + // test page: https://exhentai.org/g/2122174/fd2525031e + let r = Regex::new(r#""#).unwrap(); + let h = r#"
007
008"#; + + let mut iter = r.captures_iter(h); + let first = iter.next().unwrap(); + println!("{}", first.get(1).unwrap().as_str()); + + let second = iter.next().unwrap(); + println!("{}", second.get(1).unwrap().as_str()); + } +} diff --git a/eh2telegraph/src/collector/mod.rs b/eh2telegraph/src/collector/mod.rs new file mode 100644 index 0000000..185e493 --- /dev/null +++ b/eh2telegraph/src/collector/mod.rs @@ -0,0 +1,99 @@ +//! Built-in collectors and trait. + +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::stream::AsyncStream; + +use self::{e_hentai::EHCollector, exhentai::EXCollector, nhentai::NHCollector}; + +pub mod utils; + +pub mod e_hentai; +pub mod exhentai; +pub mod nhentai; +pub mod pixiv; + +#[derive(Debug, Clone)] +pub struct ImageMeta { + pub id: String, + pub url: String, + pub description: Option, +} + +pub type ImageData = bytes::Bytes; + +#[derive(Debug, Clone)] +pub struct AlbumMeta { + pub link: String, + pub name: String, + pub class: Option, + pub description: Option, + pub authors: Option>, + pub tags: Option>, +} + +/// Generic collector. +/// The `async fetch` returns the result of `AlbumMeta` and `ImageStream`. +/// By exposing `ImageStream`, we can fetch the images lazily. For low +/// memory VM, it will keep only a small amount in memory. +pub trait Collector { + type FetchError; + type FetchFuture<'a>: std::future::Future< + Output = Result<(AlbumMeta, Self::ImageStream), Self::FetchError>, + > + where + Self: 'a; + + type StreamError; + type ImageStream: AsyncStream>; + + fn name() -> &'static str; + fn fetch(&self, path: String) -> Self::FetchFuture<'_>; +} + +pub(crate) static URL_FROM_TEXT_RE: Lazy = Lazy::new(|| { + Regex::new(r#"((https://exhentai\.org/g/\w+/[\w-]+)|(https://e-hentai\.org/g/\w+/[\w-]+)|(https://nhentai\.net/g/\d+)|(https://nhentai\.to/g/\d+))"#).unwrap() +}); +pub(crate) static URL_FROM_URL_RE: Lazy = Lazy::new(|| { + Regex::new(r#"^((https://exhentai\.org/g/\w+/[\w-]+)|(https://e-hentai\.org/g/\w+/[\w-]+)|(https://nhentai\.net/g/\d+)|(https://nhentai\.to/g/\d+))"#).unwrap() +}); + +#[derive(Debug, Clone)] +pub struct Registry { + eh: EHCollector, + nh: NHCollector, + ex: EXCollector, +} + +pub trait Param { + fn get(&self) -> &T; +} + +impl Param for Registry { + fn get(&self) -> &EHCollector { + &self.eh + } +} + +impl Param for Registry { + fn get(&self) -> &NHCollector { + &self.nh + } +} + +impl Param for Registry { + fn get(&self) -> &EXCollector { + &self.ex + } +} + +impl Registry { + pub fn new_from_config() -> Self { + Self { + eh: EHCollector::new_from_config().expect("unable to build e-hentai collector"), + nh: NHCollector::new_from_config().expect("unable to build nhentai collector"), + ex: EXCollector::new_from_config().expect("unable to build exhentai collector"), + } + } +} diff --git a/eh2telegraph/src/collector/nhentai.rs b/eh2telegraph/src/collector/nhentai.rs new file mode 100644 index 0000000..950b1b0 --- /dev/null +++ b/eh2telegraph/src/collector/nhentai.rs @@ -0,0 +1,176 @@ +/// nhentai collector. +/// Host matching: nhentai.to or nhentai.net +use again::RetryPolicy; +use ipnet::Ipv6Net; +use regex::Regex; +use reqwest::Response; +use std::time::Duration; + +use crate::{ + http_client::{GhostClient, GhostClientBuilder}, + stream::AsyncStream, + util::get_bytes, + util::match_first_group, +}; + +use super::{AlbumMeta, Collector, ImageData, ImageMeta}; + +lazy_static::lazy_static! { + static ref TITLE_RE: Regex = Regex::new(r#"(.*?)"#).unwrap(); + static ref PAGE_RE: Regex = Regex::new(r#"