├── .dockerignore ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── docker.yaml │ └── release.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── config.toml ├── justfile └── src ├── app.rs ├── commands.rs ├── commands ├── info.rs ├── poll.rs └── push.rs ├── config.rs ├── database.rs ├── lib.rs ├── main.rs ├── telegram.rs ├── twitter.rs └── twitter ├── timeline.rs └── users.rs /.dockerignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /fixture 3 | /rocksdb 4 | /target 5 | .env 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | pull_request: 4 | push: 5 | paths-ignore: 6 | - .github 7 | - config.toml 8 | - Dockerfile 9 | - justfile 10 | - README.md 11 | 12 | name: Test and Build 13 | 14 | jobs: 15 | test: 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | rust-toolchain: [stable] 20 | fail-fast: false 21 | runs-on: ${{ matrix.os }} 22 | steps: 23 | - uses: extractions/setup-just@v1 24 | - name: Checkout code 25 | uses: actions/checkout@v3 26 | - name: Install Rust toolchain 27 | uses: actions-rs/toolchain@v1 28 | with: 29 | toolchain: ${{ matrix.rust-toolchain }} 30 | profile: minimal 31 | components: rustfmt,clippy 32 | override: true 33 | - uses: rui314/setup-mold@v1 34 | - name: Cache build artifacts 35 | uses: Swatinem/rust-cache@v2 36 | - name: Install cargo-nextest 37 | uses: baptiste0928/cargo-install@v1 38 | with: 39 | crate: cargo-nextest 40 | - name: Build code 41 | run: just build 42 | - name: Lint code 43 | uses: actions-rs/clippy-check@v1 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | args: --all-features -- -W clippy::all 47 | - name: Test code 48 | run: cargo nextest run --all-targets --test-threads=1 49 | -------------------------------------------------------------------------------- /.github/workflows/docker.yaml: -------------------------------------------------------------------------------- 1 | name: Build container image 2 | on: 3 | workflow_dispatch: 4 | push: 5 | tags: 6 | - "v*" 7 | jobs: 8 | build: 9 | name: Build container image 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Docker meta 15 | id: meta 16 | uses: docker/metadata-action@v4 17 | with: 18 | images: ghcr.io/${{ github.repository }} 19 | tags: | 20 | type=semver,pattern={{version}} 21 | flavor: | 22 | latest=true 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v2 25 | - name: Login to GitHub Container Registry 26 | if: github.event_name != 'pull_request' 27 | uses: docker/login-action@v2 28 | with: 29 | registry: ghcr.io 30 | username: ${{ github.actor }} 31 | password: ${{ secrets.CONTAINER_TOKEN }} 32 | - name: Build and push image 33 | uses: docker/build-push-action@v3 34 | with: 35 | context: . 36 | platforms: linux/amd64 37 | push: ${{ github.event_name != 'pull_request' }} 38 | tags: ${{ steps.meta.outputs.tags }} 39 | labels: ${{ steps.meta.outputs.labels }} 40 | cache-from: type=gha 41 | cache-to: type=gha,mode=max 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # CI that: 2 | # 3 | # * checks for a Git Tag that looks like a release 4 | # * creates a Github Release™ and fills in its text 5 | # * builds artifacts with cargo-dist (executable-zips, installers) 6 | # * uploads those artifacts to the Github Release™ 7 | # 8 | # Note that the Github Release™ will be created before the artifacts, 9 | # so there will be a few minutes where the release has no artifacts 10 | # and then they will slowly trickle in, possibly failing. To make 11 | # this more pleasant we mark the release as a "draft" until all 12 | # artifacts have been successfully uploaded. This allows you to 13 | # choose what to do with partial successes and avoids spamming 14 | # anyone with notifications before the release is actually ready. 15 | name: Release 16 | 17 | permissions: 18 | contents: write 19 | 20 | # This task will run whenever you push a git tag that looks like a version 21 | # like "v1", "v1.2.0", "v0.1.0-prerelease01", "my-app-v1.0.0", etc. 22 | # The version will be roughly parsed as ({PACKAGE_NAME}-)?v{VERSION}, where 23 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 24 | # must be a Cargo-style SemVer Version. 25 | # 26 | # If PACKAGE_NAME is specified, then we will create a Github Release™ for that 27 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 28 | # 29 | # If PACKAGE_NAME isn't specified, then we will create a Github Release™ for all 30 | # (cargo-dist-able) packages in the workspace with that version (this is mode is 31 | # intended for workspaces with only one dist-able package, or with all dist-able 32 | # packages versioned/released in lockstep). 33 | # 34 | # If you push multiple tags at once, separate instances of this workflow will 35 | # spin up, creating an independent Github Release™ for each one. 36 | # 37 | # If there's a prerelease-style suffix to the version then the Github Release™ 38 | # will be marked as a prerelease. 39 | on: 40 | push: 41 | tags: 42 | - '*-?v[0-9]+*' 43 | 44 | jobs: 45 | # Create the Github Release™ so the packages have something to be uploaded to 46 | create-release: 47 | runs-on: ubuntu-latest 48 | outputs: 49 | has-releases: ${{ steps.create-release.outputs.has-releases }} 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | steps: 53 | - uses: actions/checkout@v3 54 | - name: Install Rust 55 | run: rustup update 1.67.1 --no-self-update && rustup default 1.67.1 56 | - name: Install cargo-dist 57 | run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 58 | - id: create-release 59 | run: | 60 | cargo dist plan --tag=${{ github.ref_name }} --output-format=json > dist-manifest.json 61 | echo "dist plan ran successfully" 62 | cat dist-manifest.json 63 | 64 | # Create the Github Release™ based on what cargo-dist thinks it should be 65 | ANNOUNCEMENT_TITLE=$(jq --raw-output ".announcement_title" dist-manifest.json) 66 | IS_PRERELEASE=$(jq --raw-output ".announcement_is_prerelease" dist-manifest.json) 67 | jq --raw-output ".announcement_github_body" dist-manifest.json > new_dist_announcement.md 68 | gh release create ${{ github.ref_name }} --draft --prerelease="$IS_PRERELEASE" --title="$ANNOUNCEMENT_TITLE" --notes-file=new_dist_announcement.md 69 | echo "created announcement!" 70 | 71 | # Upload the manifest to the Github Release™ 72 | gh release upload ${{ github.ref_name }} dist-manifest.json 73 | echo "uploaded manifest!" 74 | 75 | # Disable all the upload-artifacts tasks if we have no actual releases 76 | HAS_RELEASES=$(jq --raw-output ".releases != null" dist-manifest.json) 77 | echo "has-releases=$HAS_RELEASES" >> "$GITHUB_OUTPUT" 78 | 79 | # Build and packages all the things 80 | upload-artifacts: 81 | # Let the initial task tell us to not run (currently very blunt) 82 | needs: create-release 83 | if: ${{ needs.create-release.outputs.has-releases == 'true' }} 84 | strategy: 85 | matrix: 86 | # For these target platforms 87 | include: 88 | - os: ubuntu-20.04 89 | dist-args: --artifacts=global 90 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 91 | - os: macos-11 92 | dist-args: --artifacts=local --target=aarch64-apple-darwin --target=x86_64-apple-darwin 93 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 94 | - os: ubuntu-20.04 95 | dist-args: --artifacts=local --target=x86_64-unknown-linux-gnu 96 | install-dist: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.0.7/cargo-dist-installer.sh | sh 97 | 98 | runs-on: ${{ matrix.os }} 99 | env: 100 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 101 | steps: 102 | - uses: actions/checkout@v3 103 | - name: Install Rust 104 | run: rustup update 1.67.1 --no-self-update && rustup default 1.67.1 105 | - name: Install cargo-dist 106 | run: ${{ matrix.install-dist }} 107 | - name: Run cargo-dist 108 | # This logic is a bit janky because it's trying to be a polyglot between 109 | # powershell and bash since this will run on windows, macos, and linux! 110 | # The two platforms don't agree on how to talk about env vars but they 111 | # do agree on 'cat' and '$()' so we use that to marshal values between commands. 112 | run: | 113 | # Actually do builds and make zips and whatnot 114 | cargo dist build --tag=${{ github.ref_name }} --output-format=json ${{ matrix.dist-args }} > dist-manifest.json 115 | echo "dist ran successfully" 116 | cat dist-manifest.json 117 | 118 | # Parse out what we just built and upload it to the Github Release™ 119 | jq --raw-output ".artifacts[]?.path | select( . != null )" dist-manifest.json > uploads.txt 120 | echo "uploading..." 121 | cat uploads.txt 122 | gh release upload ${{ github.ref_name }} $(cat uploads.txt) 123 | echo "uploaded!" 124 | 125 | # Mark the Github Release™ as a non-draft now that everything has succeeded! 126 | publish-release: 127 | # Only run after all the other tasks, but it's ok if upload-artifacts was skipped 128 | needs: [create-release, upload-artifacts] 129 | if: ${{ always() && needs.create-release.result == 'success' && (needs.upload-artifacts.result == 'skipped' || needs.upload-artifacts.result == 'success') }} 130 | runs-on: ubuntu-latest 131 | env: 132 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | steps: 134 | - uses: actions/checkout@v3 135 | - name: mark release as non-draft 136 | run: | 137 | gh release edit ${{ github.ref_name }} --draft=false 138 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /fixture 3 | /rocksdb 4 | .env 5 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.20.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "android-tzdata" 22 | version = "0.1.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 25 | 26 | [[package]] 27 | name = "android_system_properties" 28 | version = "0.1.5" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 31 | dependencies = [ 32 | "libc", 33 | ] 34 | 35 | [[package]] 36 | name = "anstream" 37 | version = "0.6.18" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 40 | dependencies = [ 41 | "anstyle", 42 | "anstyle-parse", 43 | "anstyle-query", 44 | "anstyle-wincon", 45 | "colorchoice", 46 | "is_terminal_polyfill", 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle" 52 | version = "1.0.8" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 55 | 56 | [[package]] 57 | name = "anstyle-parse" 58 | version = "0.2.0" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" 61 | dependencies = [ 62 | "utf8parse", 63 | ] 64 | 65 | [[package]] 66 | name = "anstyle-query" 67 | version = "1.0.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" 70 | dependencies = [ 71 | "windows-sys 0.48.0", 72 | ] 73 | 74 | [[package]] 75 | name = "anstyle-wincon" 76 | version = "3.0.6" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" 79 | dependencies = [ 80 | "anstyle", 81 | "windows-sys 0.59.0", 82 | ] 83 | 84 | [[package]] 85 | name = "anyhow" 86 | version = "1.0.95" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" 89 | 90 | [[package]] 91 | name = "autocfg" 92 | version = "1.1.0" 93 | source = "registry+https://github.com/rust-lang/crates.io-index" 94 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 95 | 96 | [[package]] 97 | name = "backtrace" 98 | version = "0.3.68" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" 101 | dependencies = [ 102 | "addr2line", 103 | "cc", 104 | "cfg-if", 105 | "libc", 106 | "miniz_oxide", 107 | "object", 108 | "rustc-demangle", 109 | ] 110 | 111 | [[package]] 112 | name = "base64" 113 | version = "0.22.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" 116 | 117 | [[package]] 118 | name = "bindgen" 119 | version = "0.69.4" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" 122 | dependencies = [ 123 | "bitflags 2.4.1", 124 | "cexpr", 125 | "clang-sys", 126 | "itertools", 127 | "lazy_static", 128 | "lazycell", 129 | "proc-macro2", 130 | "quote", 131 | "regex", 132 | "rustc-hash", 133 | "shlex", 134 | "syn 2.0.85", 135 | ] 136 | 137 | [[package]] 138 | name = "bitflags" 139 | version = "1.3.2" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 142 | 143 | [[package]] 144 | name = "bitflags" 145 | version = "2.4.1" 146 | source = "registry+https://github.com/rust-lang/crates.io-index" 147 | checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" 148 | 149 | [[package]] 150 | name = "bumpalo" 151 | version = "3.12.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" 154 | 155 | [[package]] 156 | name = "bytecount" 157 | version = "0.6.3" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c" 160 | 161 | [[package]] 162 | name = "bytes" 163 | version = "1.9.0" 164 | source = "registry+https://github.com/rust-lang/crates.io-index" 165 | checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" 166 | 167 | [[package]] 168 | name = "bzip2-sys" 169 | version = "0.1.11+1.0.8" 170 | source = "registry+https://github.com/rust-lang/crates.io-index" 171 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 172 | dependencies = [ 173 | "cc", 174 | "libc", 175 | "pkg-config", 176 | ] 177 | 178 | [[package]] 179 | name = "cc" 180 | version = "1.2.6" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "8d6dbb628b8f8555f86d0323c2eb39e3ec81901f4b83e091db8a6a76d316a333" 183 | dependencies = [ 184 | "jobserver", 185 | "libc", 186 | "shlex", 187 | ] 188 | 189 | [[package]] 190 | name = "cexpr" 191 | version = "0.6.0" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 194 | dependencies = [ 195 | "nom", 196 | ] 197 | 198 | [[package]] 199 | name = "cfg-if" 200 | version = "1.0.0" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 203 | 204 | [[package]] 205 | name = "chrono" 206 | version = "0.4.39" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" 209 | dependencies = [ 210 | "android-tzdata", 211 | "iana-time-zone", 212 | "js-sys", 213 | "num-traits", 214 | "wasm-bindgen", 215 | "windows-targets 0.52.6", 216 | ] 217 | 218 | [[package]] 219 | name = "clang-sys" 220 | version = "1.4.0" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" 223 | dependencies = [ 224 | "glob", 225 | "libc", 226 | "libloading", 227 | ] 228 | 229 | [[package]] 230 | name = "clap" 231 | version = "4.5.27" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "769b0145982b4b48713e01ec42d61614425f27b7058bda7180a3a41f30104796" 234 | dependencies = [ 235 | "clap_builder", 236 | "clap_derive", 237 | ] 238 | 239 | [[package]] 240 | name = "clap_builder" 241 | version = "4.5.27" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "1b26884eb4b57140e4d2d93652abfa49498b938b3c9179f9fc487b0acc3edad7" 244 | dependencies = [ 245 | "anstream", 246 | "anstyle", 247 | "clap_lex", 248 | "strsim", 249 | ] 250 | 251 | [[package]] 252 | name = "clap_derive" 253 | version = "4.5.24" 254 | source = "registry+https://github.com/rust-lang/crates.io-index" 255 | checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" 256 | dependencies = [ 257 | "heck 0.5.0", 258 | "proc-macro2", 259 | "quote", 260 | "syn 2.0.85", 261 | ] 262 | 263 | [[package]] 264 | name = "clap_lex" 265 | version = "0.7.4" 266 | source = "registry+https://github.com/rust-lang/crates.io-index" 267 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 268 | 269 | [[package]] 270 | name = "codespan-reporting" 271 | version = "0.11.1" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" 274 | dependencies = [ 275 | "termcolor", 276 | "unicode-width 0.1.11", 277 | ] 278 | 279 | [[package]] 280 | name = "colorchoice" 281 | version = "1.0.0" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" 284 | 285 | [[package]] 286 | name = "core-foundation" 287 | version = "0.9.3" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" 290 | dependencies = [ 291 | "core-foundation-sys", 292 | "libc", 293 | ] 294 | 295 | [[package]] 296 | name = "core-foundation-sys" 297 | version = "0.8.3" 298 | source = "registry+https://github.com/rust-lang/crates.io-index" 299 | checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" 300 | 301 | [[package]] 302 | name = "cxx" 303 | version = "1.0.89" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" 306 | dependencies = [ 307 | "cc", 308 | "cxxbridge-flags", 309 | "cxxbridge-macro", 310 | "link-cplusplus", 311 | ] 312 | 313 | [[package]] 314 | name = "cxx-build" 315 | version = "1.0.89" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" 318 | dependencies = [ 319 | "cc", 320 | "codespan-reporting", 321 | "once_cell", 322 | "proc-macro2", 323 | "quote", 324 | "scratch", 325 | "syn 1.0.107", 326 | ] 327 | 328 | [[package]] 329 | name = "cxxbridge-flags" 330 | version = "1.0.89" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" 333 | 334 | [[package]] 335 | name = "cxxbridge-macro" 336 | version = "1.0.89" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" 339 | dependencies = [ 340 | "proc-macro2", 341 | "quote", 342 | "syn 1.0.107", 343 | ] 344 | 345 | [[package]] 346 | name = "displaydoc" 347 | version = "0.2.5" 348 | source = "registry+https://github.com/rust-lang/crates.io-index" 349 | checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" 350 | dependencies = [ 351 | "proc-macro2", 352 | "quote", 353 | "syn 2.0.85", 354 | ] 355 | 356 | [[package]] 357 | name = "either" 358 | version = "1.8.1" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 361 | 362 | [[package]] 363 | name = "encoding_rs" 364 | version = "0.8.32" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" 367 | dependencies = [ 368 | "cfg-if", 369 | ] 370 | 371 | [[package]] 372 | name = "equivalent" 373 | version = "1.0.0" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" 376 | 377 | [[package]] 378 | name = "fastrand" 379 | version = "1.8.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" 382 | dependencies = [ 383 | "instant", 384 | ] 385 | 386 | [[package]] 387 | name = "fnv" 388 | version = "1.0.7" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 391 | 392 | [[package]] 393 | name = "foreign-types" 394 | version = "0.3.2" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 397 | dependencies = [ 398 | "foreign-types-shared", 399 | ] 400 | 401 | [[package]] 402 | name = "foreign-types-shared" 403 | version = "0.1.1" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 406 | 407 | [[package]] 408 | name = "form_urlencoded" 409 | version = "1.2.1" 410 | source = "registry+https://github.com/rust-lang/crates.io-index" 411 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 412 | dependencies = [ 413 | "percent-encoding", 414 | ] 415 | 416 | [[package]] 417 | name = "futures-channel" 418 | version = "0.3.26" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" 421 | dependencies = [ 422 | "futures-core", 423 | ] 424 | 425 | [[package]] 426 | name = "futures-core" 427 | version = "0.3.30" 428 | source = "registry+https://github.com/rust-lang/crates.io-index" 429 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 430 | 431 | [[package]] 432 | name = "futures-sink" 433 | version = "0.3.26" 434 | source = "registry+https://github.com/rust-lang/crates.io-index" 435 | checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" 436 | 437 | [[package]] 438 | name = "futures-task" 439 | version = "0.3.30" 440 | source = "registry+https://github.com/rust-lang/crates.io-index" 441 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 442 | 443 | [[package]] 444 | name = "futures-util" 445 | version = "0.3.30" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 448 | dependencies = [ 449 | "futures-core", 450 | "futures-task", 451 | "pin-project-lite", 452 | "pin-utils", 453 | ] 454 | 455 | [[package]] 456 | name = "getrandom" 457 | version = "0.2.14" 458 | source = "registry+https://github.com/rust-lang/crates.io-index" 459 | checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" 460 | dependencies = [ 461 | "cfg-if", 462 | "libc", 463 | "wasi", 464 | ] 465 | 466 | [[package]] 467 | name = "gimli" 468 | version = "0.27.3" 469 | source = "registry+https://github.com/rust-lang/crates.io-index" 470 | checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" 471 | 472 | [[package]] 473 | name = "glob" 474 | version = "0.3.1" 475 | source = "registry+https://github.com/rust-lang/crates.io-index" 476 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 477 | 478 | [[package]] 479 | name = "h2" 480 | version = "0.4.4" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" 483 | dependencies = [ 484 | "bytes", 485 | "fnv", 486 | "futures-core", 487 | "futures-sink", 488 | "futures-util", 489 | "http", 490 | "indexmap", 491 | "slab", 492 | "tokio", 493 | "tokio-util", 494 | "tracing", 495 | ] 496 | 497 | [[package]] 498 | name = "hashbrown" 499 | version = "0.14.0" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" 502 | 503 | [[package]] 504 | name = "heck" 505 | version = "0.4.1" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 508 | 509 | [[package]] 510 | name = "heck" 511 | version = "0.5.0" 512 | source = "registry+https://github.com/rust-lang/crates.io-index" 513 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 514 | 515 | [[package]] 516 | name = "hermit-abi" 517 | version = "0.3.9" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 520 | 521 | [[package]] 522 | name = "http" 523 | version = "1.1.0" 524 | source = "registry+https://github.com/rust-lang/crates.io-index" 525 | checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" 526 | dependencies = [ 527 | "bytes", 528 | "fnv", 529 | "itoa", 530 | ] 531 | 532 | [[package]] 533 | name = "http-body" 534 | version = "1.0.0" 535 | source = "registry+https://github.com/rust-lang/crates.io-index" 536 | checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" 537 | dependencies = [ 538 | "bytes", 539 | "http", 540 | ] 541 | 542 | [[package]] 543 | name = "http-body-util" 544 | version = "0.1.1" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" 547 | dependencies = [ 548 | "bytes", 549 | "futures-core", 550 | "http", 551 | "http-body", 552 | "pin-project-lite", 553 | ] 554 | 555 | [[package]] 556 | name = "httparse" 557 | version = "1.8.0" 558 | source = "registry+https://github.com/rust-lang/crates.io-index" 559 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 560 | 561 | [[package]] 562 | name = "hyper" 563 | version = "1.5.2" 564 | source = "registry+https://github.com/rust-lang/crates.io-index" 565 | checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" 566 | dependencies = [ 567 | "bytes", 568 | "futures-channel", 569 | "futures-util", 570 | "h2", 571 | "http", 572 | "http-body", 573 | "httparse", 574 | "itoa", 575 | "pin-project-lite", 576 | "smallvec", 577 | "tokio", 578 | "want", 579 | ] 580 | 581 | [[package]] 582 | name = "hyper-rustls" 583 | version = "0.27.2" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" 586 | dependencies = [ 587 | "futures-util", 588 | "http", 589 | "hyper", 590 | "hyper-util", 591 | "rustls", 592 | "rustls-pki-types", 593 | "tokio", 594 | "tokio-rustls", 595 | "tower-service", 596 | ] 597 | 598 | [[package]] 599 | name = "hyper-tls" 600 | version = "0.6.0" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" 603 | dependencies = [ 604 | "bytes", 605 | "http-body-util", 606 | "hyper", 607 | "hyper-util", 608 | "native-tls", 609 | "tokio", 610 | "tokio-native-tls", 611 | "tower-service", 612 | ] 613 | 614 | [[package]] 615 | name = "hyper-util" 616 | version = "0.1.10" 617 | source = "registry+https://github.com/rust-lang/crates.io-index" 618 | checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" 619 | dependencies = [ 620 | "bytes", 621 | "futures-channel", 622 | "futures-util", 623 | "http", 624 | "http-body", 625 | "hyper", 626 | "pin-project-lite", 627 | "socket2", 628 | "tokio", 629 | "tower-service", 630 | "tracing", 631 | ] 632 | 633 | [[package]] 634 | name = "iana-time-zone" 635 | version = "0.1.53" 636 | source = "registry+https://github.com/rust-lang/crates.io-index" 637 | checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" 638 | dependencies = [ 639 | "android_system_properties", 640 | "core-foundation-sys", 641 | "iana-time-zone-haiku", 642 | "js-sys", 643 | "wasm-bindgen", 644 | "winapi", 645 | ] 646 | 647 | [[package]] 648 | name = "iana-time-zone-haiku" 649 | version = "0.1.1" 650 | source = "registry+https://github.com/rust-lang/crates.io-index" 651 | checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" 652 | dependencies = [ 653 | "cxx", 654 | "cxx-build", 655 | ] 656 | 657 | [[package]] 658 | name = "icu_collections" 659 | version = "1.5.0" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" 662 | dependencies = [ 663 | "displaydoc", 664 | "yoke", 665 | "zerofrom", 666 | "zerovec", 667 | ] 668 | 669 | [[package]] 670 | name = "icu_locid" 671 | version = "1.5.0" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" 674 | dependencies = [ 675 | "displaydoc", 676 | "litemap", 677 | "tinystr", 678 | "writeable", 679 | "zerovec", 680 | ] 681 | 682 | [[package]] 683 | name = "icu_locid_transform" 684 | version = "1.5.0" 685 | source = "registry+https://github.com/rust-lang/crates.io-index" 686 | checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" 687 | dependencies = [ 688 | "displaydoc", 689 | "icu_locid", 690 | "icu_locid_transform_data", 691 | "icu_provider", 692 | "tinystr", 693 | "zerovec", 694 | ] 695 | 696 | [[package]] 697 | name = "icu_locid_transform_data" 698 | version = "1.5.0" 699 | source = "registry+https://github.com/rust-lang/crates.io-index" 700 | checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" 701 | 702 | [[package]] 703 | name = "icu_normalizer" 704 | version = "1.5.0" 705 | source = "registry+https://github.com/rust-lang/crates.io-index" 706 | checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" 707 | dependencies = [ 708 | "displaydoc", 709 | "icu_collections", 710 | "icu_normalizer_data", 711 | "icu_properties", 712 | "icu_provider", 713 | "smallvec", 714 | "utf16_iter", 715 | "utf8_iter", 716 | "write16", 717 | "zerovec", 718 | ] 719 | 720 | [[package]] 721 | name = "icu_normalizer_data" 722 | version = "1.5.0" 723 | source = "registry+https://github.com/rust-lang/crates.io-index" 724 | checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" 725 | 726 | [[package]] 727 | name = "icu_properties" 728 | version = "1.5.1" 729 | source = "registry+https://github.com/rust-lang/crates.io-index" 730 | checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" 731 | dependencies = [ 732 | "displaydoc", 733 | "icu_collections", 734 | "icu_locid_transform", 735 | "icu_properties_data", 736 | "icu_provider", 737 | "tinystr", 738 | "zerovec", 739 | ] 740 | 741 | [[package]] 742 | name = "icu_properties_data" 743 | version = "1.5.0" 744 | source = "registry+https://github.com/rust-lang/crates.io-index" 745 | checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" 746 | 747 | [[package]] 748 | name = "icu_provider" 749 | version = "1.5.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" 752 | dependencies = [ 753 | "displaydoc", 754 | "icu_locid", 755 | "icu_provider_macros", 756 | "stable_deref_trait", 757 | "tinystr", 758 | "writeable", 759 | "yoke", 760 | "zerofrom", 761 | "zerovec", 762 | ] 763 | 764 | [[package]] 765 | name = "icu_provider_macros" 766 | version = "1.5.0" 767 | source = "registry+https://github.com/rust-lang/crates.io-index" 768 | checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" 769 | dependencies = [ 770 | "proc-macro2", 771 | "quote", 772 | "syn 2.0.85", 773 | ] 774 | 775 | [[package]] 776 | name = "idna" 777 | version = "1.0.3" 778 | source = "registry+https://github.com/rust-lang/crates.io-index" 779 | checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" 780 | dependencies = [ 781 | "idna_adapter", 782 | "smallvec", 783 | "utf8_iter", 784 | ] 785 | 786 | [[package]] 787 | name = "idna_adapter" 788 | version = "1.2.0" 789 | source = "registry+https://github.com/rust-lang/crates.io-index" 790 | checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" 791 | dependencies = [ 792 | "icu_normalizer", 793 | "icu_properties", 794 | ] 795 | 796 | [[package]] 797 | name = "indexmap" 798 | version = "2.0.0" 799 | source = "registry+https://github.com/rust-lang/crates.io-index" 800 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" 801 | dependencies = [ 802 | "equivalent", 803 | "hashbrown", 804 | ] 805 | 806 | [[package]] 807 | name = "instant" 808 | version = "0.1.12" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 811 | dependencies = [ 812 | "cfg-if", 813 | ] 814 | 815 | [[package]] 816 | name = "ipnet" 817 | version = "2.7.1" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" 820 | 821 | [[package]] 822 | name = "is_terminal_polyfill" 823 | version = "1.70.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 826 | 827 | [[package]] 828 | name = "itertools" 829 | version = "0.12.1" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 832 | dependencies = [ 833 | "either", 834 | ] 835 | 836 | [[package]] 837 | name = "itoa" 838 | version = "1.0.5" 839 | source = "registry+https://github.com/rust-lang/crates.io-index" 840 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 841 | 842 | [[package]] 843 | name = "jobserver" 844 | version = "0.1.31" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" 847 | dependencies = [ 848 | "libc", 849 | ] 850 | 851 | [[package]] 852 | name = "js-sys" 853 | version = "0.3.61" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" 856 | dependencies = [ 857 | "wasm-bindgen", 858 | ] 859 | 860 | [[package]] 861 | name = "lazy_static" 862 | version = "1.4.0" 863 | source = "registry+https://github.com/rust-lang/crates.io-index" 864 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 865 | 866 | [[package]] 867 | name = "lazycell" 868 | version = "1.3.0" 869 | source = "registry+https://github.com/rust-lang/crates.io-index" 870 | checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" 871 | 872 | [[package]] 873 | name = "libc" 874 | version = "0.2.169" 875 | source = "registry+https://github.com/rust-lang/crates.io-index" 876 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 877 | 878 | [[package]] 879 | name = "libloading" 880 | version = "0.7.4" 881 | source = "registry+https://github.com/rust-lang/crates.io-index" 882 | checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" 883 | dependencies = [ 884 | "cfg-if", 885 | "winapi", 886 | ] 887 | 888 | [[package]] 889 | name = "librocksdb-sys" 890 | version = "0.17.1+9.9.3" 891 | source = "registry+https://github.com/rust-lang/crates.io-index" 892 | checksum = "2b7869a512ae9982f4d46ba482c2a304f1efd80c6412a3d4bf57bb79a619679f" 893 | dependencies = [ 894 | "bindgen", 895 | "bzip2-sys", 896 | "cc", 897 | "libc", 898 | "libz-sys", 899 | "lz4-sys", 900 | "zstd-sys", 901 | ] 902 | 903 | [[package]] 904 | name = "libz-sys" 905 | version = "1.1.8" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" 908 | dependencies = [ 909 | "cc", 910 | "pkg-config", 911 | "vcpkg", 912 | ] 913 | 914 | [[package]] 915 | name = "link-cplusplus" 916 | version = "1.0.8" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" 919 | dependencies = [ 920 | "cc", 921 | ] 922 | 923 | [[package]] 924 | name = "litemap" 925 | version = "0.7.3" 926 | source = "registry+https://github.com/rust-lang/crates.io-index" 927 | checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" 928 | 929 | [[package]] 930 | name = "lock_api" 931 | version = "0.4.9" 932 | source = "registry+https://github.com/rust-lang/crates.io-index" 933 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 934 | dependencies = [ 935 | "autocfg", 936 | "scopeguard", 937 | ] 938 | 939 | [[package]] 940 | name = "log" 941 | version = "0.4.17" 942 | source = "registry+https://github.com/rust-lang/crates.io-index" 943 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 944 | dependencies = [ 945 | "cfg-if", 946 | ] 947 | 948 | [[package]] 949 | name = "lz4-sys" 950 | version = "1.11.1+lz4-1.10.0" 951 | source = "registry+https://github.com/rust-lang/crates.io-index" 952 | checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" 953 | dependencies = [ 954 | "cc", 955 | "libc", 956 | ] 957 | 958 | [[package]] 959 | name = "matchers" 960 | version = "0.1.0" 961 | source = "registry+https://github.com/rust-lang/crates.io-index" 962 | checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" 963 | dependencies = [ 964 | "regex-automata", 965 | ] 966 | 967 | [[package]] 968 | name = "memchr" 969 | version = "2.5.0" 970 | source = "registry+https://github.com/rust-lang/crates.io-index" 971 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 972 | 973 | [[package]] 974 | name = "mime" 975 | version = "0.3.16" 976 | source = "registry+https://github.com/rust-lang/crates.io-index" 977 | checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" 978 | 979 | [[package]] 980 | name = "minimal-lexical" 981 | version = "0.2.1" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 984 | 985 | [[package]] 986 | name = "miniz_oxide" 987 | version = "0.7.1" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" 990 | dependencies = [ 991 | "adler", 992 | ] 993 | 994 | [[package]] 995 | name = "mio" 996 | version = "1.0.1" 997 | source = "registry+https://github.com/rust-lang/crates.io-index" 998 | checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" 999 | dependencies = [ 1000 | "hermit-abi", 1001 | "libc", 1002 | "wasi", 1003 | "windows-sys 0.52.0", 1004 | ] 1005 | 1006 | [[package]] 1007 | name = "native-tls" 1008 | version = "0.2.11" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 1011 | dependencies = [ 1012 | "lazy_static", 1013 | "libc", 1014 | "log", 1015 | "openssl", 1016 | "openssl-probe", 1017 | "openssl-sys", 1018 | "schannel", 1019 | "security-framework", 1020 | "security-framework-sys", 1021 | "tempfile", 1022 | ] 1023 | 1024 | [[package]] 1025 | name = "nom" 1026 | version = "7.1.3" 1027 | source = "registry+https://github.com/rust-lang/crates.io-index" 1028 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1029 | dependencies = [ 1030 | "memchr", 1031 | "minimal-lexical", 1032 | ] 1033 | 1034 | [[package]] 1035 | name = "nu-ansi-term" 1036 | version = "0.46.0" 1037 | source = "registry+https://github.com/rust-lang/crates.io-index" 1038 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 1039 | dependencies = [ 1040 | "overload", 1041 | "winapi", 1042 | ] 1043 | 1044 | [[package]] 1045 | name = "num-traits" 1046 | version = "0.2.15" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" 1049 | dependencies = [ 1050 | "autocfg", 1051 | ] 1052 | 1053 | [[package]] 1054 | name = "object" 1055 | version = "0.31.1" 1056 | source = "registry+https://github.com/rust-lang/crates.io-index" 1057 | checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" 1058 | dependencies = [ 1059 | "memchr", 1060 | ] 1061 | 1062 | [[package]] 1063 | name = "once_cell" 1064 | version = "1.19.0" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 1067 | 1068 | [[package]] 1069 | name = "openssl" 1070 | version = "0.10.66" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" 1073 | dependencies = [ 1074 | "bitflags 2.4.1", 1075 | "cfg-if", 1076 | "foreign-types", 1077 | "libc", 1078 | "once_cell", 1079 | "openssl-macros", 1080 | "openssl-sys", 1081 | ] 1082 | 1083 | [[package]] 1084 | name = "openssl-macros" 1085 | version = "0.1.0" 1086 | source = "registry+https://github.com/rust-lang/crates.io-index" 1087 | checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" 1088 | dependencies = [ 1089 | "proc-macro2", 1090 | "quote", 1091 | "syn 1.0.107", 1092 | ] 1093 | 1094 | [[package]] 1095 | name = "openssl-probe" 1096 | version = "0.1.5" 1097 | source = "registry+https://github.com/rust-lang/crates.io-index" 1098 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 1099 | 1100 | [[package]] 1101 | name = "openssl-sys" 1102 | version = "0.9.103" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" 1105 | dependencies = [ 1106 | "cc", 1107 | "libc", 1108 | "pkg-config", 1109 | "vcpkg", 1110 | ] 1111 | 1112 | [[package]] 1113 | name = "overload" 1114 | version = "0.1.1" 1115 | source = "registry+https://github.com/rust-lang/crates.io-index" 1116 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 1117 | 1118 | [[package]] 1119 | name = "papergrid" 1120 | version = "0.13.0" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "d2b0f8def1f117e13c895f3eda65a7b5650688da29d6ad04635f61bc7b92eebd" 1123 | dependencies = [ 1124 | "bytecount", 1125 | "fnv", 1126 | "unicode-width 0.2.0", 1127 | ] 1128 | 1129 | [[package]] 1130 | name = "parking_lot" 1131 | version = "0.12.1" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1134 | dependencies = [ 1135 | "lock_api", 1136 | "parking_lot_core", 1137 | ] 1138 | 1139 | [[package]] 1140 | name = "parking_lot_core" 1141 | version = "0.9.7" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 1144 | dependencies = [ 1145 | "cfg-if", 1146 | "libc", 1147 | "redox_syscall", 1148 | "smallvec", 1149 | "windows-sys 0.45.0", 1150 | ] 1151 | 1152 | [[package]] 1153 | name = "percent-encoding" 1154 | version = "2.3.1" 1155 | source = "registry+https://github.com/rust-lang/crates.io-index" 1156 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1157 | 1158 | [[package]] 1159 | name = "pigeon" 1160 | version = "0.2.22" 1161 | dependencies = [ 1162 | "anyhow", 1163 | "chrono", 1164 | "clap", 1165 | "reqwest", 1166 | "rocksdb", 1167 | "serde", 1168 | "serde_json", 1169 | "tabled", 1170 | "test-log", 1171 | "tokio", 1172 | "toml", 1173 | "tracing", 1174 | "tracing-subscriber", 1175 | "url", 1176 | ] 1177 | 1178 | [[package]] 1179 | name = "pin-project-lite" 1180 | version = "0.2.12" 1181 | source = "registry+https://github.com/rust-lang/crates.io-index" 1182 | checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" 1183 | 1184 | [[package]] 1185 | name = "pin-utils" 1186 | version = "0.1.0" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1189 | 1190 | [[package]] 1191 | name = "pkg-config" 1192 | version = "0.3.26" 1193 | source = "registry+https://github.com/rust-lang/crates.io-index" 1194 | checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" 1195 | 1196 | [[package]] 1197 | name = "proc-macro-error-attr2" 1198 | version = "2.0.0" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" 1201 | dependencies = [ 1202 | "proc-macro2", 1203 | "quote", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "proc-macro-error2" 1208 | version = "2.0.1" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" 1211 | dependencies = [ 1212 | "proc-macro-error-attr2", 1213 | "proc-macro2", 1214 | "quote", 1215 | "syn 2.0.85", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "proc-macro2" 1220 | version = "1.0.89" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" 1223 | dependencies = [ 1224 | "unicode-ident", 1225 | ] 1226 | 1227 | [[package]] 1228 | name = "quote" 1229 | version = "1.0.35" 1230 | source = "registry+https://github.com/rust-lang/crates.io-index" 1231 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 1232 | dependencies = [ 1233 | "proc-macro2", 1234 | ] 1235 | 1236 | [[package]] 1237 | name = "redox_syscall" 1238 | version = "0.2.16" 1239 | source = "registry+https://github.com/rust-lang/crates.io-index" 1240 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 1241 | dependencies = [ 1242 | "bitflags 1.3.2", 1243 | ] 1244 | 1245 | [[package]] 1246 | name = "regex" 1247 | version = "1.7.1" 1248 | source = "registry+https://github.com/rust-lang/crates.io-index" 1249 | checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" 1250 | dependencies = [ 1251 | "regex-syntax", 1252 | ] 1253 | 1254 | [[package]] 1255 | name = "regex-automata" 1256 | version = "0.1.10" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" 1259 | dependencies = [ 1260 | "regex-syntax", 1261 | ] 1262 | 1263 | [[package]] 1264 | name = "regex-syntax" 1265 | version = "0.6.28" 1266 | source = "registry+https://github.com/rust-lang/crates.io-index" 1267 | checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" 1268 | 1269 | [[package]] 1270 | name = "remove_dir_all" 1271 | version = "0.5.3" 1272 | source = "registry+https://github.com/rust-lang/crates.io-index" 1273 | checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" 1274 | dependencies = [ 1275 | "winapi", 1276 | ] 1277 | 1278 | [[package]] 1279 | name = "reqwest" 1280 | version = "0.12.12" 1281 | source = "registry+https://github.com/rust-lang/crates.io-index" 1282 | checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" 1283 | dependencies = [ 1284 | "base64", 1285 | "bytes", 1286 | "encoding_rs", 1287 | "futures-core", 1288 | "futures-util", 1289 | "h2", 1290 | "http", 1291 | "http-body", 1292 | "http-body-util", 1293 | "hyper", 1294 | "hyper-rustls", 1295 | "hyper-tls", 1296 | "hyper-util", 1297 | "ipnet", 1298 | "js-sys", 1299 | "log", 1300 | "mime", 1301 | "native-tls", 1302 | "once_cell", 1303 | "percent-encoding", 1304 | "pin-project-lite", 1305 | "rustls-pemfile", 1306 | "serde", 1307 | "serde_json", 1308 | "serde_urlencoded", 1309 | "sync_wrapper", 1310 | "system-configuration", 1311 | "tokio", 1312 | "tokio-native-tls", 1313 | "tokio-socks", 1314 | "tower", 1315 | "tower-service", 1316 | "url", 1317 | "wasm-bindgen", 1318 | "wasm-bindgen-futures", 1319 | "web-sys", 1320 | "windows-registry", 1321 | ] 1322 | 1323 | [[package]] 1324 | name = "ring" 1325 | version = "0.17.8" 1326 | source = "registry+https://github.com/rust-lang/crates.io-index" 1327 | checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" 1328 | dependencies = [ 1329 | "cc", 1330 | "cfg-if", 1331 | "getrandom", 1332 | "libc", 1333 | "spin", 1334 | "untrusted", 1335 | "windows-sys 0.52.0", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "rocksdb" 1340 | version = "0.23.0" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "26ec73b20525cb235bad420f911473b69f9fe27cc856c5461bccd7e4af037f43" 1343 | dependencies = [ 1344 | "libc", 1345 | "librocksdb-sys", 1346 | ] 1347 | 1348 | [[package]] 1349 | name = "rustc-demangle" 1350 | version = "0.1.23" 1351 | source = "registry+https://github.com/rust-lang/crates.io-index" 1352 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" 1353 | 1354 | [[package]] 1355 | name = "rustc-hash" 1356 | version = "1.1.0" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1359 | 1360 | [[package]] 1361 | name = "rustls" 1362 | version = "0.23.7" 1363 | source = "registry+https://github.com/rust-lang/crates.io-index" 1364 | checksum = "ebbbdb961df0ad3f2652da8f3fdc4b36122f568f968f45ad3316f26c025c677b" 1365 | dependencies = [ 1366 | "once_cell", 1367 | "rustls-pki-types", 1368 | "rustls-webpki", 1369 | "subtle", 1370 | "zeroize", 1371 | ] 1372 | 1373 | [[package]] 1374 | name = "rustls-pemfile" 1375 | version = "2.1.2" 1376 | source = "registry+https://github.com/rust-lang/crates.io-index" 1377 | checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" 1378 | dependencies = [ 1379 | "base64", 1380 | "rustls-pki-types", 1381 | ] 1382 | 1383 | [[package]] 1384 | name = "rustls-pki-types" 1385 | version = "1.4.1" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" 1388 | 1389 | [[package]] 1390 | name = "rustls-webpki" 1391 | version = "0.102.3" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" 1394 | dependencies = [ 1395 | "ring", 1396 | "rustls-pki-types", 1397 | "untrusted", 1398 | ] 1399 | 1400 | [[package]] 1401 | name = "ryu" 1402 | version = "1.0.12" 1403 | source = "registry+https://github.com/rust-lang/crates.io-index" 1404 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 1405 | 1406 | [[package]] 1407 | name = "schannel" 1408 | version = "0.1.21" 1409 | source = "registry+https://github.com/rust-lang/crates.io-index" 1410 | checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" 1411 | dependencies = [ 1412 | "windows-sys 0.42.0", 1413 | ] 1414 | 1415 | [[package]] 1416 | name = "scopeguard" 1417 | version = "1.1.0" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 1420 | 1421 | [[package]] 1422 | name = "scratch" 1423 | version = "1.0.3" 1424 | source = "registry+https://github.com/rust-lang/crates.io-index" 1425 | checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" 1426 | 1427 | [[package]] 1428 | name = "security-framework" 1429 | version = "2.8.2" 1430 | source = "registry+https://github.com/rust-lang/crates.io-index" 1431 | checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" 1432 | dependencies = [ 1433 | "bitflags 1.3.2", 1434 | "core-foundation", 1435 | "core-foundation-sys", 1436 | "libc", 1437 | "security-framework-sys", 1438 | ] 1439 | 1440 | [[package]] 1441 | name = "security-framework-sys" 1442 | version = "2.8.0" 1443 | source = "registry+https://github.com/rust-lang/crates.io-index" 1444 | checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" 1445 | dependencies = [ 1446 | "core-foundation-sys", 1447 | "libc", 1448 | ] 1449 | 1450 | [[package]] 1451 | name = "serde" 1452 | version = "1.0.217" 1453 | source = "registry+https://github.com/rust-lang/crates.io-index" 1454 | checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" 1455 | dependencies = [ 1456 | "serde_derive", 1457 | ] 1458 | 1459 | [[package]] 1460 | name = "serde_derive" 1461 | version = "1.0.217" 1462 | source = "registry+https://github.com/rust-lang/crates.io-index" 1463 | checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" 1464 | dependencies = [ 1465 | "proc-macro2", 1466 | "quote", 1467 | "syn 2.0.85", 1468 | ] 1469 | 1470 | [[package]] 1471 | name = "serde_json" 1472 | version = "1.0.137" 1473 | source = "registry+https://github.com/rust-lang/crates.io-index" 1474 | checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" 1475 | dependencies = [ 1476 | "itoa", 1477 | "memchr", 1478 | "ryu", 1479 | "serde", 1480 | ] 1481 | 1482 | [[package]] 1483 | name = "serde_spanned" 1484 | version = "0.6.7" 1485 | source = "registry+https://github.com/rust-lang/crates.io-index" 1486 | checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" 1487 | dependencies = [ 1488 | "serde", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "serde_urlencoded" 1493 | version = "0.7.1" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1496 | dependencies = [ 1497 | "form_urlencoded", 1498 | "itoa", 1499 | "ryu", 1500 | "serde", 1501 | ] 1502 | 1503 | [[package]] 1504 | name = "sharded-slab" 1505 | version = "0.1.4" 1506 | source = "registry+https://github.com/rust-lang/crates.io-index" 1507 | checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" 1508 | dependencies = [ 1509 | "lazy_static", 1510 | ] 1511 | 1512 | [[package]] 1513 | name = "shlex" 1514 | version = "1.3.0" 1515 | source = "registry+https://github.com/rust-lang/crates.io-index" 1516 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1517 | 1518 | [[package]] 1519 | name = "signal-hook-registry" 1520 | version = "1.4.0" 1521 | source = "registry+https://github.com/rust-lang/crates.io-index" 1522 | checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" 1523 | dependencies = [ 1524 | "libc", 1525 | ] 1526 | 1527 | [[package]] 1528 | name = "slab" 1529 | version = "0.4.7" 1530 | source = "registry+https://github.com/rust-lang/crates.io-index" 1531 | checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" 1532 | dependencies = [ 1533 | "autocfg", 1534 | ] 1535 | 1536 | [[package]] 1537 | name = "smallvec" 1538 | version = "1.13.2" 1539 | source = "registry+https://github.com/rust-lang/crates.io-index" 1540 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 1541 | 1542 | [[package]] 1543 | name = "socket2" 1544 | version = "0.5.5" 1545 | source = "registry+https://github.com/rust-lang/crates.io-index" 1546 | checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" 1547 | dependencies = [ 1548 | "libc", 1549 | "windows-sys 0.48.0", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "spin" 1554 | version = "0.9.8" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" 1557 | 1558 | [[package]] 1559 | name = "stable_deref_trait" 1560 | version = "1.2.0" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1563 | 1564 | [[package]] 1565 | name = "strsim" 1566 | version = "0.11.0" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" 1569 | 1570 | [[package]] 1571 | name = "subtle" 1572 | version = "2.5.0" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" 1575 | 1576 | [[package]] 1577 | name = "syn" 1578 | version = "1.0.107" 1579 | source = "registry+https://github.com/rust-lang/crates.io-index" 1580 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 1581 | dependencies = [ 1582 | "proc-macro2", 1583 | "quote", 1584 | "unicode-ident", 1585 | ] 1586 | 1587 | [[package]] 1588 | name = "syn" 1589 | version = "2.0.85" 1590 | source = "registry+https://github.com/rust-lang/crates.io-index" 1591 | checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" 1592 | dependencies = [ 1593 | "proc-macro2", 1594 | "quote", 1595 | "unicode-ident", 1596 | ] 1597 | 1598 | [[package]] 1599 | name = "sync_wrapper" 1600 | version = "1.0.1" 1601 | source = "registry+https://github.com/rust-lang/crates.io-index" 1602 | checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" 1603 | dependencies = [ 1604 | "futures-core", 1605 | ] 1606 | 1607 | [[package]] 1608 | name = "synstructure" 1609 | version = "0.13.1" 1610 | source = "registry+https://github.com/rust-lang/crates.io-index" 1611 | checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" 1612 | dependencies = [ 1613 | "proc-macro2", 1614 | "quote", 1615 | "syn 2.0.85", 1616 | ] 1617 | 1618 | [[package]] 1619 | name = "system-configuration" 1620 | version = "0.6.0" 1621 | source = "registry+https://github.com/rust-lang/crates.io-index" 1622 | checksum = "658bc6ee10a9b4fcf576e9b0819d95ec16f4d2c02d39fd83ac1c8789785c4a42" 1623 | dependencies = [ 1624 | "bitflags 2.4.1", 1625 | "core-foundation", 1626 | "system-configuration-sys", 1627 | ] 1628 | 1629 | [[package]] 1630 | name = "system-configuration-sys" 1631 | version = "0.6.0" 1632 | source = "registry+https://github.com/rust-lang/crates.io-index" 1633 | checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" 1634 | dependencies = [ 1635 | "core-foundation-sys", 1636 | "libc", 1637 | ] 1638 | 1639 | [[package]] 1640 | name = "tabled" 1641 | version = "0.17.0" 1642 | source = "registry+https://github.com/rust-lang/crates.io-index" 1643 | checksum = "c6709222f3973137427ce50559cd564dc187a95b9cfe01613d2f4e93610e510a" 1644 | dependencies = [ 1645 | "papergrid", 1646 | "tabled_derive", 1647 | ] 1648 | 1649 | [[package]] 1650 | name = "tabled_derive" 1651 | version = "0.9.0" 1652 | source = "registry+https://github.com/rust-lang/crates.io-index" 1653 | checksum = "931be476627d4c54070a1f3a9739ccbfec9b36b39815106a20cce2243bbcefe1" 1654 | dependencies = [ 1655 | "heck 0.4.1", 1656 | "proc-macro-error2", 1657 | "proc-macro2", 1658 | "quote", 1659 | "syn 1.0.107", 1660 | ] 1661 | 1662 | [[package]] 1663 | name = "tempfile" 1664 | version = "3.3.0" 1665 | source = "registry+https://github.com/rust-lang/crates.io-index" 1666 | checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" 1667 | dependencies = [ 1668 | "cfg-if", 1669 | "fastrand", 1670 | "libc", 1671 | "redox_syscall", 1672 | "remove_dir_all", 1673 | "winapi", 1674 | ] 1675 | 1676 | [[package]] 1677 | name = "termcolor" 1678 | version = "1.2.0" 1679 | source = "registry+https://github.com/rust-lang/crates.io-index" 1680 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 1681 | dependencies = [ 1682 | "winapi-util", 1683 | ] 1684 | 1685 | [[package]] 1686 | name = "test-log" 1687 | version = "0.2.17" 1688 | source = "registry+https://github.com/rust-lang/crates.io-index" 1689 | checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" 1690 | dependencies = [ 1691 | "test-log-macros", 1692 | "tracing-subscriber", 1693 | ] 1694 | 1695 | [[package]] 1696 | name = "test-log-macros" 1697 | version = "0.2.15" 1698 | source = "registry+https://github.com/rust-lang/crates.io-index" 1699 | checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107" 1700 | dependencies = [ 1701 | "proc-macro2", 1702 | "quote", 1703 | "syn 2.0.85", 1704 | ] 1705 | 1706 | [[package]] 1707 | name = "thiserror" 1708 | version = "1.0.38" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" 1711 | dependencies = [ 1712 | "thiserror-impl", 1713 | ] 1714 | 1715 | [[package]] 1716 | name = "thiserror-impl" 1717 | version = "1.0.38" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" 1720 | dependencies = [ 1721 | "proc-macro2", 1722 | "quote", 1723 | "syn 1.0.107", 1724 | ] 1725 | 1726 | [[package]] 1727 | name = "thread_local" 1728 | version = "1.1.5" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "d805f96b7e61fce8728f142e213a3eb2f6b10538efff2d637c923efa4bfca048" 1731 | dependencies = [ 1732 | "cfg-if", 1733 | "once_cell", 1734 | ] 1735 | 1736 | [[package]] 1737 | name = "tinystr" 1738 | version = "0.7.6" 1739 | source = "registry+https://github.com/rust-lang/crates.io-index" 1740 | checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" 1741 | dependencies = [ 1742 | "displaydoc", 1743 | "zerovec", 1744 | ] 1745 | 1746 | [[package]] 1747 | name = "tokio" 1748 | version = "1.43.0" 1749 | source = "registry+https://github.com/rust-lang/crates.io-index" 1750 | checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" 1751 | dependencies = [ 1752 | "backtrace", 1753 | "bytes", 1754 | "libc", 1755 | "mio", 1756 | "parking_lot", 1757 | "pin-project-lite", 1758 | "signal-hook-registry", 1759 | "socket2", 1760 | "tokio-macros", 1761 | "windows-sys 0.52.0", 1762 | ] 1763 | 1764 | [[package]] 1765 | name = "tokio-macros" 1766 | version = "2.5.0" 1767 | source = "registry+https://github.com/rust-lang/crates.io-index" 1768 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" 1769 | dependencies = [ 1770 | "proc-macro2", 1771 | "quote", 1772 | "syn 2.0.85", 1773 | ] 1774 | 1775 | [[package]] 1776 | name = "tokio-native-tls" 1777 | version = "0.3.1" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" 1780 | dependencies = [ 1781 | "native-tls", 1782 | "tokio", 1783 | ] 1784 | 1785 | [[package]] 1786 | name = "tokio-rustls" 1787 | version = "0.26.0" 1788 | source = "registry+https://github.com/rust-lang/crates.io-index" 1789 | checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" 1790 | dependencies = [ 1791 | "rustls", 1792 | "rustls-pki-types", 1793 | "tokio", 1794 | ] 1795 | 1796 | [[package]] 1797 | name = "tokio-socks" 1798 | version = "0.5.2" 1799 | source = "registry+https://github.com/rust-lang/crates.io-index" 1800 | checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" 1801 | dependencies = [ 1802 | "either", 1803 | "futures-util", 1804 | "thiserror", 1805 | "tokio", 1806 | ] 1807 | 1808 | [[package]] 1809 | name = "tokio-util" 1810 | version = "0.7.4" 1811 | source = "registry+https://github.com/rust-lang/crates.io-index" 1812 | checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" 1813 | dependencies = [ 1814 | "bytes", 1815 | "futures-core", 1816 | "futures-sink", 1817 | "pin-project-lite", 1818 | "tokio", 1819 | "tracing", 1820 | ] 1821 | 1822 | [[package]] 1823 | name = "toml" 1824 | version = "0.8.19" 1825 | source = "registry+https://github.com/rust-lang/crates.io-index" 1826 | checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" 1827 | dependencies = [ 1828 | "serde", 1829 | "serde_spanned", 1830 | "toml_datetime", 1831 | "toml_edit", 1832 | ] 1833 | 1834 | [[package]] 1835 | name = "toml_datetime" 1836 | version = "0.6.8" 1837 | source = "registry+https://github.com/rust-lang/crates.io-index" 1838 | checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" 1839 | dependencies = [ 1840 | "serde", 1841 | ] 1842 | 1843 | [[package]] 1844 | name = "toml_edit" 1845 | version = "0.22.20" 1846 | source = "registry+https://github.com/rust-lang/crates.io-index" 1847 | checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" 1848 | dependencies = [ 1849 | "indexmap", 1850 | "serde", 1851 | "serde_spanned", 1852 | "toml_datetime", 1853 | "winnow", 1854 | ] 1855 | 1856 | [[package]] 1857 | name = "tower" 1858 | version = "0.5.2" 1859 | source = "registry+https://github.com/rust-lang/crates.io-index" 1860 | checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" 1861 | dependencies = [ 1862 | "futures-core", 1863 | "futures-util", 1864 | "pin-project-lite", 1865 | "sync_wrapper", 1866 | "tokio", 1867 | "tower-layer", 1868 | "tower-service", 1869 | ] 1870 | 1871 | [[package]] 1872 | name = "tower-layer" 1873 | version = "0.3.3" 1874 | source = "registry+https://github.com/rust-lang/crates.io-index" 1875 | checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" 1876 | 1877 | [[package]] 1878 | name = "tower-service" 1879 | version = "0.3.3" 1880 | source = "registry+https://github.com/rust-lang/crates.io-index" 1881 | checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 1882 | 1883 | [[package]] 1884 | name = "tracing" 1885 | version = "0.1.41" 1886 | source = "registry+https://github.com/rust-lang/crates.io-index" 1887 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 1888 | dependencies = [ 1889 | "pin-project-lite", 1890 | "tracing-attributes", 1891 | "tracing-core", 1892 | ] 1893 | 1894 | [[package]] 1895 | name = "tracing-attributes" 1896 | version = "0.1.28" 1897 | source = "registry+https://github.com/rust-lang/crates.io-index" 1898 | checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" 1899 | dependencies = [ 1900 | "proc-macro2", 1901 | "quote", 1902 | "syn 2.0.85", 1903 | ] 1904 | 1905 | [[package]] 1906 | name = "tracing-core" 1907 | version = "0.1.33" 1908 | source = "registry+https://github.com/rust-lang/crates.io-index" 1909 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 1910 | dependencies = [ 1911 | "once_cell", 1912 | "valuable", 1913 | ] 1914 | 1915 | [[package]] 1916 | name = "tracing-log" 1917 | version = "0.2.0" 1918 | source = "registry+https://github.com/rust-lang/crates.io-index" 1919 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 1920 | dependencies = [ 1921 | "log", 1922 | "once_cell", 1923 | "tracing-core", 1924 | ] 1925 | 1926 | [[package]] 1927 | name = "tracing-subscriber" 1928 | version = "0.3.19" 1929 | source = "registry+https://github.com/rust-lang/crates.io-index" 1930 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 1931 | dependencies = [ 1932 | "matchers", 1933 | "nu-ansi-term", 1934 | "once_cell", 1935 | "regex", 1936 | "sharded-slab", 1937 | "smallvec", 1938 | "thread_local", 1939 | "tracing", 1940 | "tracing-core", 1941 | "tracing-log", 1942 | ] 1943 | 1944 | [[package]] 1945 | name = "try-lock" 1946 | version = "0.2.4" 1947 | source = "registry+https://github.com/rust-lang/crates.io-index" 1948 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1949 | 1950 | [[package]] 1951 | name = "unicode-ident" 1952 | version = "1.0.6" 1953 | source = "registry+https://github.com/rust-lang/crates.io-index" 1954 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 1955 | 1956 | [[package]] 1957 | name = "unicode-width" 1958 | version = "0.1.11" 1959 | source = "registry+https://github.com/rust-lang/crates.io-index" 1960 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1961 | 1962 | [[package]] 1963 | name = "unicode-width" 1964 | version = "0.2.0" 1965 | source = "registry+https://github.com/rust-lang/crates.io-index" 1966 | checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" 1967 | 1968 | [[package]] 1969 | name = "untrusted" 1970 | version = "0.9.0" 1971 | source = "registry+https://github.com/rust-lang/crates.io-index" 1972 | checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" 1973 | 1974 | [[package]] 1975 | name = "url" 1976 | version = "2.5.4" 1977 | source = "registry+https://github.com/rust-lang/crates.io-index" 1978 | checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" 1979 | dependencies = [ 1980 | "form_urlencoded", 1981 | "idna", 1982 | "percent-encoding", 1983 | ] 1984 | 1985 | [[package]] 1986 | name = "utf16_iter" 1987 | version = "1.0.5" 1988 | source = "registry+https://github.com/rust-lang/crates.io-index" 1989 | checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" 1990 | 1991 | [[package]] 1992 | name = "utf8_iter" 1993 | version = "1.0.4" 1994 | source = "registry+https://github.com/rust-lang/crates.io-index" 1995 | checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" 1996 | 1997 | [[package]] 1998 | name = "utf8parse" 1999 | version = "0.2.1" 2000 | source = "registry+https://github.com/rust-lang/crates.io-index" 2001 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" 2002 | 2003 | [[package]] 2004 | name = "valuable" 2005 | version = "0.1.0" 2006 | source = "registry+https://github.com/rust-lang/crates.io-index" 2007 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 2008 | 2009 | [[package]] 2010 | name = "vcpkg" 2011 | version = "0.2.15" 2012 | source = "registry+https://github.com/rust-lang/crates.io-index" 2013 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 2014 | 2015 | [[package]] 2016 | name = "want" 2017 | version = "0.3.0" 2018 | source = "registry+https://github.com/rust-lang/crates.io-index" 2019 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 2020 | dependencies = [ 2021 | "log", 2022 | "try-lock", 2023 | ] 2024 | 2025 | [[package]] 2026 | name = "wasi" 2027 | version = "0.11.0+wasi-snapshot-preview1" 2028 | source = "registry+https://github.com/rust-lang/crates.io-index" 2029 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 2030 | 2031 | [[package]] 2032 | name = "wasm-bindgen" 2033 | version = "0.2.93" 2034 | source = "registry+https://github.com/rust-lang/crates.io-index" 2035 | checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" 2036 | dependencies = [ 2037 | "cfg-if", 2038 | "once_cell", 2039 | "wasm-bindgen-macro", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "wasm-bindgen-backend" 2044 | version = "0.2.93" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" 2047 | dependencies = [ 2048 | "bumpalo", 2049 | "log", 2050 | "once_cell", 2051 | "proc-macro2", 2052 | "quote", 2053 | "syn 2.0.85", 2054 | "wasm-bindgen-shared", 2055 | ] 2056 | 2057 | [[package]] 2058 | name = "wasm-bindgen-futures" 2059 | version = "0.4.34" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" 2062 | dependencies = [ 2063 | "cfg-if", 2064 | "js-sys", 2065 | "wasm-bindgen", 2066 | "web-sys", 2067 | ] 2068 | 2069 | [[package]] 2070 | name = "wasm-bindgen-macro" 2071 | version = "0.2.93" 2072 | source = "registry+https://github.com/rust-lang/crates.io-index" 2073 | checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" 2074 | dependencies = [ 2075 | "quote", 2076 | "wasm-bindgen-macro-support", 2077 | ] 2078 | 2079 | [[package]] 2080 | name = "wasm-bindgen-macro-support" 2081 | version = "0.2.93" 2082 | source = "registry+https://github.com/rust-lang/crates.io-index" 2083 | checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" 2084 | dependencies = [ 2085 | "proc-macro2", 2086 | "quote", 2087 | "syn 2.0.85", 2088 | "wasm-bindgen-backend", 2089 | "wasm-bindgen-shared", 2090 | ] 2091 | 2092 | [[package]] 2093 | name = "wasm-bindgen-shared" 2094 | version = "0.2.93" 2095 | source = "registry+https://github.com/rust-lang/crates.io-index" 2096 | checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" 2097 | 2098 | [[package]] 2099 | name = "web-sys" 2100 | version = "0.3.61" 2101 | source = "registry+https://github.com/rust-lang/crates.io-index" 2102 | checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" 2103 | dependencies = [ 2104 | "js-sys", 2105 | "wasm-bindgen", 2106 | ] 2107 | 2108 | [[package]] 2109 | name = "winapi" 2110 | version = "0.3.9" 2111 | source = "registry+https://github.com/rust-lang/crates.io-index" 2112 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2113 | dependencies = [ 2114 | "winapi-i686-pc-windows-gnu", 2115 | "winapi-x86_64-pc-windows-gnu", 2116 | ] 2117 | 2118 | [[package]] 2119 | name = "winapi-i686-pc-windows-gnu" 2120 | version = "0.4.0" 2121 | source = "registry+https://github.com/rust-lang/crates.io-index" 2122 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2123 | 2124 | [[package]] 2125 | name = "winapi-util" 2126 | version = "0.1.5" 2127 | source = "registry+https://github.com/rust-lang/crates.io-index" 2128 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 2129 | dependencies = [ 2130 | "winapi", 2131 | ] 2132 | 2133 | [[package]] 2134 | name = "winapi-x86_64-pc-windows-gnu" 2135 | version = "0.4.0" 2136 | source = "registry+https://github.com/rust-lang/crates.io-index" 2137 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2138 | 2139 | [[package]] 2140 | name = "windows-registry" 2141 | version = "0.2.0" 2142 | source = "registry+https://github.com/rust-lang/crates.io-index" 2143 | checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" 2144 | dependencies = [ 2145 | "windows-result", 2146 | "windows-strings", 2147 | "windows-targets 0.52.6", 2148 | ] 2149 | 2150 | [[package]] 2151 | name = "windows-result" 2152 | version = "0.2.0" 2153 | source = "registry+https://github.com/rust-lang/crates.io-index" 2154 | checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" 2155 | dependencies = [ 2156 | "windows-targets 0.52.6", 2157 | ] 2158 | 2159 | [[package]] 2160 | name = "windows-strings" 2161 | version = "0.1.0" 2162 | source = "registry+https://github.com/rust-lang/crates.io-index" 2163 | checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" 2164 | dependencies = [ 2165 | "windows-result", 2166 | "windows-targets 0.52.6", 2167 | ] 2168 | 2169 | [[package]] 2170 | name = "windows-sys" 2171 | version = "0.42.0" 2172 | source = "registry+https://github.com/rust-lang/crates.io-index" 2173 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 2174 | dependencies = [ 2175 | "windows_aarch64_gnullvm 0.42.1", 2176 | "windows_aarch64_msvc 0.42.1", 2177 | "windows_i686_gnu 0.42.1", 2178 | "windows_i686_msvc 0.42.1", 2179 | "windows_x86_64_gnu 0.42.1", 2180 | "windows_x86_64_gnullvm 0.42.1", 2181 | "windows_x86_64_msvc 0.42.1", 2182 | ] 2183 | 2184 | [[package]] 2185 | name = "windows-sys" 2186 | version = "0.45.0" 2187 | source = "registry+https://github.com/rust-lang/crates.io-index" 2188 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 2189 | dependencies = [ 2190 | "windows-targets 0.42.1", 2191 | ] 2192 | 2193 | [[package]] 2194 | name = "windows-sys" 2195 | version = "0.48.0" 2196 | source = "registry+https://github.com/rust-lang/crates.io-index" 2197 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2198 | dependencies = [ 2199 | "windows-targets 0.48.0", 2200 | ] 2201 | 2202 | [[package]] 2203 | name = "windows-sys" 2204 | version = "0.52.0" 2205 | source = "registry+https://github.com/rust-lang/crates.io-index" 2206 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2207 | dependencies = [ 2208 | "windows-targets 0.52.6", 2209 | ] 2210 | 2211 | [[package]] 2212 | name = "windows-sys" 2213 | version = "0.59.0" 2214 | source = "registry+https://github.com/rust-lang/crates.io-index" 2215 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2216 | dependencies = [ 2217 | "windows-targets 0.52.6", 2218 | ] 2219 | 2220 | [[package]] 2221 | name = "windows-targets" 2222 | version = "0.42.1" 2223 | source = "registry+https://github.com/rust-lang/crates.io-index" 2224 | checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" 2225 | dependencies = [ 2226 | "windows_aarch64_gnullvm 0.42.1", 2227 | "windows_aarch64_msvc 0.42.1", 2228 | "windows_i686_gnu 0.42.1", 2229 | "windows_i686_msvc 0.42.1", 2230 | "windows_x86_64_gnu 0.42.1", 2231 | "windows_x86_64_gnullvm 0.42.1", 2232 | "windows_x86_64_msvc 0.42.1", 2233 | ] 2234 | 2235 | [[package]] 2236 | name = "windows-targets" 2237 | version = "0.48.0" 2238 | source = "registry+https://github.com/rust-lang/crates.io-index" 2239 | checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" 2240 | dependencies = [ 2241 | "windows_aarch64_gnullvm 0.48.0", 2242 | "windows_aarch64_msvc 0.48.0", 2243 | "windows_i686_gnu 0.48.0", 2244 | "windows_i686_msvc 0.48.0", 2245 | "windows_x86_64_gnu 0.48.0", 2246 | "windows_x86_64_gnullvm 0.48.0", 2247 | "windows_x86_64_msvc 0.48.0", 2248 | ] 2249 | 2250 | [[package]] 2251 | name = "windows-targets" 2252 | version = "0.52.6" 2253 | source = "registry+https://github.com/rust-lang/crates.io-index" 2254 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2255 | dependencies = [ 2256 | "windows_aarch64_gnullvm 0.52.6", 2257 | "windows_aarch64_msvc 0.52.6", 2258 | "windows_i686_gnu 0.52.6", 2259 | "windows_i686_gnullvm", 2260 | "windows_i686_msvc 0.52.6", 2261 | "windows_x86_64_gnu 0.52.6", 2262 | "windows_x86_64_gnullvm 0.52.6", 2263 | "windows_x86_64_msvc 0.52.6", 2264 | ] 2265 | 2266 | [[package]] 2267 | name = "windows_aarch64_gnullvm" 2268 | version = "0.42.1" 2269 | source = "registry+https://github.com/rust-lang/crates.io-index" 2270 | checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" 2271 | 2272 | [[package]] 2273 | name = "windows_aarch64_gnullvm" 2274 | version = "0.48.0" 2275 | source = "registry+https://github.com/rust-lang/crates.io-index" 2276 | checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" 2277 | 2278 | [[package]] 2279 | name = "windows_aarch64_gnullvm" 2280 | version = "0.52.6" 2281 | source = "registry+https://github.com/rust-lang/crates.io-index" 2282 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2283 | 2284 | [[package]] 2285 | name = "windows_aarch64_msvc" 2286 | version = "0.42.1" 2287 | source = "registry+https://github.com/rust-lang/crates.io-index" 2288 | checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" 2289 | 2290 | [[package]] 2291 | name = "windows_aarch64_msvc" 2292 | version = "0.48.0" 2293 | source = "registry+https://github.com/rust-lang/crates.io-index" 2294 | checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" 2295 | 2296 | [[package]] 2297 | name = "windows_aarch64_msvc" 2298 | version = "0.52.6" 2299 | source = "registry+https://github.com/rust-lang/crates.io-index" 2300 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2301 | 2302 | [[package]] 2303 | name = "windows_i686_gnu" 2304 | version = "0.42.1" 2305 | source = "registry+https://github.com/rust-lang/crates.io-index" 2306 | checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" 2307 | 2308 | [[package]] 2309 | name = "windows_i686_gnu" 2310 | version = "0.48.0" 2311 | source = "registry+https://github.com/rust-lang/crates.io-index" 2312 | checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" 2313 | 2314 | [[package]] 2315 | name = "windows_i686_gnu" 2316 | version = "0.52.6" 2317 | source = "registry+https://github.com/rust-lang/crates.io-index" 2318 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2319 | 2320 | [[package]] 2321 | name = "windows_i686_gnullvm" 2322 | version = "0.52.6" 2323 | source = "registry+https://github.com/rust-lang/crates.io-index" 2324 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2325 | 2326 | [[package]] 2327 | name = "windows_i686_msvc" 2328 | version = "0.42.1" 2329 | source = "registry+https://github.com/rust-lang/crates.io-index" 2330 | checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" 2331 | 2332 | [[package]] 2333 | name = "windows_i686_msvc" 2334 | version = "0.48.0" 2335 | source = "registry+https://github.com/rust-lang/crates.io-index" 2336 | checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" 2337 | 2338 | [[package]] 2339 | name = "windows_i686_msvc" 2340 | version = "0.52.6" 2341 | source = "registry+https://github.com/rust-lang/crates.io-index" 2342 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2343 | 2344 | [[package]] 2345 | name = "windows_x86_64_gnu" 2346 | version = "0.42.1" 2347 | source = "registry+https://github.com/rust-lang/crates.io-index" 2348 | checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" 2349 | 2350 | [[package]] 2351 | name = "windows_x86_64_gnu" 2352 | version = "0.48.0" 2353 | source = "registry+https://github.com/rust-lang/crates.io-index" 2354 | checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" 2355 | 2356 | [[package]] 2357 | name = "windows_x86_64_gnu" 2358 | version = "0.52.6" 2359 | source = "registry+https://github.com/rust-lang/crates.io-index" 2360 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2361 | 2362 | [[package]] 2363 | name = "windows_x86_64_gnullvm" 2364 | version = "0.42.1" 2365 | source = "registry+https://github.com/rust-lang/crates.io-index" 2366 | checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" 2367 | 2368 | [[package]] 2369 | name = "windows_x86_64_gnullvm" 2370 | version = "0.48.0" 2371 | source = "registry+https://github.com/rust-lang/crates.io-index" 2372 | checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" 2373 | 2374 | [[package]] 2375 | name = "windows_x86_64_gnullvm" 2376 | version = "0.52.6" 2377 | source = "registry+https://github.com/rust-lang/crates.io-index" 2378 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2379 | 2380 | [[package]] 2381 | name = "windows_x86_64_msvc" 2382 | version = "0.42.1" 2383 | source = "registry+https://github.com/rust-lang/crates.io-index" 2384 | checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" 2385 | 2386 | [[package]] 2387 | name = "windows_x86_64_msvc" 2388 | version = "0.48.0" 2389 | source = "registry+https://github.com/rust-lang/crates.io-index" 2390 | checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" 2391 | 2392 | [[package]] 2393 | name = "windows_x86_64_msvc" 2394 | version = "0.52.6" 2395 | source = "registry+https://github.com/rust-lang/crates.io-index" 2396 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2397 | 2398 | [[package]] 2399 | name = "winnow" 2400 | version = "0.6.18" 2401 | source = "registry+https://github.com/rust-lang/crates.io-index" 2402 | checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" 2403 | dependencies = [ 2404 | "memchr", 2405 | ] 2406 | 2407 | [[package]] 2408 | name = "write16" 2409 | version = "1.0.0" 2410 | source = "registry+https://github.com/rust-lang/crates.io-index" 2411 | checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" 2412 | 2413 | [[package]] 2414 | name = "writeable" 2415 | version = "0.5.5" 2416 | source = "registry+https://github.com/rust-lang/crates.io-index" 2417 | checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" 2418 | 2419 | [[package]] 2420 | name = "yoke" 2421 | version = "0.7.4" 2422 | source = "registry+https://github.com/rust-lang/crates.io-index" 2423 | checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" 2424 | dependencies = [ 2425 | "serde", 2426 | "stable_deref_trait", 2427 | "yoke-derive", 2428 | "zerofrom", 2429 | ] 2430 | 2431 | [[package]] 2432 | name = "yoke-derive" 2433 | version = "0.7.4" 2434 | source = "registry+https://github.com/rust-lang/crates.io-index" 2435 | checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" 2436 | dependencies = [ 2437 | "proc-macro2", 2438 | "quote", 2439 | "syn 2.0.85", 2440 | "synstructure", 2441 | ] 2442 | 2443 | [[package]] 2444 | name = "zerofrom" 2445 | version = "0.1.4" 2446 | source = "registry+https://github.com/rust-lang/crates.io-index" 2447 | checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" 2448 | dependencies = [ 2449 | "zerofrom-derive", 2450 | ] 2451 | 2452 | [[package]] 2453 | name = "zerofrom-derive" 2454 | version = "0.1.4" 2455 | source = "registry+https://github.com/rust-lang/crates.io-index" 2456 | checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" 2457 | dependencies = [ 2458 | "proc-macro2", 2459 | "quote", 2460 | "syn 2.0.85", 2461 | "synstructure", 2462 | ] 2463 | 2464 | [[package]] 2465 | name = "zeroize" 2466 | version = "1.8.1" 2467 | source = "registry+https://github.com/rust-lang/crates.io-index" 2468 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 2469 | 2470 | [[package]] 2471 | name = "zerovec" 2472 | version = "0.10.4" 2473 | source = "registry+https://github.com/rust-lang/crates.io-index" 2474 | checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" 2475 | dependencies = [ 2476 | "yoke", 2477 | "zerofrom", 2478 | "zerovec-derive", 2479 | ] 2480 | 2481 | [[package]] 2482 | name = "zerovec-derive" 2483 | version = "0.10.3" 2484 | source = "registry+https://github.com/rust-lang/crates.io-index" 2485 | checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" 2486 | dependencies = [ 2487 | "proc-macro2", 2488 | "quote", 2489 | "syn 2.0.85", 2490 | ] 2491 | 2492 | [[package]] 2493 | name = "zstd-sys" 2494 | version = "2.0.6+zstd.1.5.2" 2495 | source = "registry+https://github.com/rust-lang/crates.io-index" 2496 | checksum = "68a3f9792c0c3dc6c165840a75f47ae1f4da402c2d006881129579f6597e801b" 2497 | dependencies = [ 2498 | "cc", 2499 | "libc", 2500 | "pkg-config", 2501 | ] 2502 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pigeon" 3 | version = "0.2.22" 4 | edition = "2021" 5 | repository = "https://github.com/williamlsh/pigeon" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | chrono = "0.4" 10 | clap = { version = "4.5", features = ["derive"] } 11 | reqwest = { version = "0.12", features = ["json"] } 12 | rocksdb = "0.23.0" 13 | serde = { version = "1.0", features = ["derive"] } 14 | serde_json = "1.0" 15 | tabled = "0.17" 16 | tokio = { version = "1.43", features = ["full"] } 17 | toml = "0.8" 18 | tracing = "0.1" 19 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 20 | url = "2.5" 21 | 22 | [features] 23 | socks = ["reqwest/socks"] 24 | 25 | [dev-dependencies] 26 | test-log = { version = "0.2", default-features = false, features = ["trace"] } 27 | 28 | # The profile that 'cargo dist' will build with 29 | [profile.dist] 30 | inherits = "release" 31 | lto = "thin" 32 | 33 | # Config for 'cargo dist' 34 | [workspace.metadata.dist] 35 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 36 | cargo-dist-version = "0.0.7" 37 | # The preferred Rust toolchain to use in CI (rustup toolchain syntax) 38 | rust-toolchain-version = "1.67.1" 39 | # CI backends to support (see 'cargo dist generate-ci') 40 | ci = ["github"] 41 | # Target platforms to build apps for (Rust target-triple syntax) 42 | targets = ["x86_64-unknown-linux-gnu", "x86_64-apple-darwin", "aarch64-apple-darwin"] 43 | # The installers to generate for each app 44 | installers = ["shell"] 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:slim AS builder 2 | 3 | RUN set -eux; \ 4 | apt-get update; \ 5 | apt-get install -y --no-install-recommends \ 6 | pkg-config \ 7 | libssl-dev \ 8 | libclang-dev \ 9 | build-essential && \ 10 | rustup component add rustfmt 11 | 12 | WORKDIR /app 13 | 14 | COPY . . 15 | 16 | RUN cargo build -r 17 | 18 | FROM debian:bullseye-slim 19 | 20 | RUN set -eux; \ 21 | apt-get update; \ 22 | apt-get install -y --no-install-recommends \ 23 | ca-certificates 24 | 25 | COPY --from=builder /app/target/release/pigeon /usr/bin/pigeon 26 | 27 | VOLUME /pigeon 28 | 29 | ENTRYPOINT [ "/usr/bin/pigeon" ] 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 William 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pigeon 2 | 3 | A Convenient Tool for Syncing Tweets to Telegram Channels. 4 | 5 | Pigeon is a powerful tool written in pure Rust that allows you to seamlessly sync Tweets to Telegram channel(s). With its user-friendly features and efficient functionality, Pigeon simplifies the process of keeping your Telegram channels up-to-date with the latest Twitter content. 6 | 7 | ## Key Features 8 | 9 | - Poll Twitter timelines 10 | - Store and display data using RocksDB 11 | - Push Tweets to Telegram channels 12 | - Interruptible and resumable pushing 13 | - No limits on the number of Twitter users and Telegram channels 14 | 15 | ## Configuration? 16 | 17 | To configure Pigeon, refer to the provided [config.toml](config.toml) file for an example setup. Additionally, you'll need to obtain the following API tokens: 18 | 19 | - Twitter API token: Visit "[How to get access to the Twitter API](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api)" for instructions on obtaining this token. 20 | - Telegram Bot API token: Follow the guide on "[Creating a new bot](https://core.telegram.org/bots/features#creating-a-new-bot.)" to acquire the necessary token. 21 | 22 | ## Usage 23 | 24 | To build the Pigeon binary, use the following command: 25 | 26 | ``` 27 | cargo build --release 28 | ``` 29 | 30 | General commands: 31 | 32 | ``` 33 | $ target/release/pigeon --help 34 | Usage: pigeon [OPTIONS] 35 | 36 | Commands: 37 | poll Poll Twitter users' timeline 38 | push Push timeline to Telegram channel(s) 39 | info Display overview information from Database 40 | help Print this message or the help of the given subcommand(s) 41 | 42 | Options: 43 | -d, --debug Activate debug mode 44 | -c, --config-path Config file path [default: config.toml] 45 | -h, --help Print help Print help information 46 | ``` 47 | 48 | Alternatively, you can download the pre-built binary from the latest [release](https://github.com/williamlsh/pigeon/releases) or utilize the [Pigeon](https://github.com/users/williamlsh/packages/container/package/pigeon) Docker image. 49 | 50 | ## Proxy Support 51 | 52 | If you require network proxy usage, build Pigeon with `socks` feature enabled: 53 | 54 | ``` 55 | cargo build --release --features socks 56 | ``` 57 | 58 | You can set up an HTTP/HTTPS or Socks5 proxy for all network connections through environment variables. For example: 59 | 60 | To use an HTTP proxy: 61 | 62 | ``` 63 | export HTTP_PROXY=http://secure.example 64 | ``` 65 | 66 | To use a Socks5 proxy: 67 | 68 | ``` 69 | export https_proxy=socks5://127.0.0.1:1086 70 | ``` 71 | 72 | ## Local Data 73 | 74 | Pigeon stores all tweet data locally in RocksDB, which resides in the specified path during runtime. Tweets pushed to Telegram channel(s) are automatically deleted, ensuring no unnecessary data clutters your disk storage. 75 | 76 | ## Author 77 | 78 | Pigeon was developed by [William](https://github.com/williamlsh), offering a robust solution for syncing Tweets to Telegram channels efficiently and effortlessly. 79 | 80 | ## License 81 | 82 | MIT License 83 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | rocksdb_path = "rocksdb" # Path to where RocksDB stores data 2 | twitter_token = "xxx" # Twitter API token 3 | telegram_token = "xxx" # Telegram Bot API token 4 | 5 | # Poll Twitter timeline(s) 6 | # Note: When Pigeon runs for the first time, it will start polling from `start_time` until `end_time`. 7 | # After the initial run, it will continue polling from where it left off based on the latest data stored in RocksDB. 8 | [[poll]] 9 | included = true # Whether to include this Twitter user's timeline for polling 10 | username = "TwitterDev" # Twitter username for this timeline 11 | max_results = 5 # Maximum number of tweets per page when polling, default is 100 12 | start_time = "2022-10-25T00:00:00.000Z" # The start time of the timeline to poll, default is the oldest time available 13 | end_time = "2022-11-01T00:00:00.000Z" # The end time of the timeline to poll, default is the current time 14 | since_id = "xyz" # The tweet ID to resume polling from (optional) 15 | 16 | # Push polled timeline data to Telegram channel(s) 17 | [[push]] 18 | included = true # Whether to include this channel for pushing 19 | from = "TwitterDev" # The Twitter user's timeline to push from 20 | username = "@some_channel_username" # The Telegram channel username (for public channels) or chat ID (for private channels), see: https://core.telegram.org/bots/api#sendmessage 21 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE := "ghcr.io/williamlsh/pigeon" 2 | IMAGE_TAG := "latest" 3 | 4 | build: 5 | @cargo build -r 6 | 7 | image: 8 | @sudo docker build -t {{DOCKER_IMAGE}}:{{IMAGE_TAG}} . 9 | 10 | push: 11 | @sudo docker push {{DOCKER_IMAGE}}:{{IMAGE_TAG}} 12 | 13 | release-ci: 14 | @cargo dist init --ci=github 15 | -------------------------------------------------------------------------------- /src/app.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | commands::{info, Poll, Push}, 3 | config::{PollConfig, PushConfig}, 4 | database::Database, 5 | Config, 6 | }; 7 | use anyhow::{anyhow, Context, Result}; 8 | use reqwest::Client; 9 | use tracing::{info, instrument}; 10 | 11 | /// Application entry. 12 | pub struct App { 13 | database: Database, 14 | client: Client, 15 | config: Config, 16 | } 17 | 18 | impl App { 19 | pub fn new(config: Config) -> Self { 20 | let database = Database::open(config.rocksdb_path.as_path()); 21 | let client = Client::new(); 22 | Self { 23 | database, 24 | client, 25 | config, 26 | } 27 | } 28 | 29 | #[instrument(skip_all)] 30 | pub async fn poll(&mut self) -> Result<()> { 31 | info!("Starting to poll Twitter timeline from config."); 32 | Poll::new( 33 | self.config.twitter_token.take(), 34 | self.poll_config()?, 35 | &self.client, 36 | &self.database, 37 | )? 38 | .run() 39 | .await 40 | .with_context(|| "Failed to execute poll command") 41 | } 42 | 43 | #[instrument(skip_all)] 44 | pub async fn push(&mut self) -> Result<()> { 45 | info!("Starting to push timeline to Telegram channel(s) from config."); 46 | Push::new( 47 | self.config.telegram_token.take(), 48 | self.push_config()?, 49 | &self.client, 50 | &mut self.database, 51 | )? 52 | .run() 53 | .await 54 | .with_context(|| "Failed to execute push command") 55 | } 56 | 57 | pub fn info(&self) -> Result<()> { 58 | info!("Overview info of database."); 59 | info(&self.database).with_context(|| "Failed to execute info command") 60 | } 61 | 62 | /// Returns poll configs that are included. 63 | fn poll_config(&mut self) -> Result> { 64 | self.config 65 | .poll 66 | .take() 67 | .map(|cfg| cfg.into_iter().filter(|cfg| cfg.included).collect()) 68 | .ok_or_else(|| anyhow!("Empty poll config")) 69 | } 70 | 71 | /// Returns push configs that are included. 72 | fn push_config(&mut self) -> Result> { 73 | self.config 74 | .push 75 | .take() 76 | .map(|cfg| cfg.into_iter().filter(|cfg| cfg.included).collect()) 77 | .ok_or_else(|| anyhow!("Empty push config")) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | mod info; 2 | mod poll; 3 | mod push; 4 | 5 | pub(crate) use info::info; 6 | pub(crate) use poll::Poll; 7 | pub(crate) use push::Push; 8 | -------------------------------------------------------------------------------- /src/commands/info.rs: -------------------------------------------------------------------------------- 1 | use std::str; 2 | use tabled::{Table, Tabled}; 3 | 4 | use crate::{database::Database, twitter::Tweet}; 5 | 6 | pub(crate) fn info(database: &Database) -> anyhow::Result<()> { 7 | display_state(database)?; 8 | display_timeline(database) 9 | } 10 | 11 | fn display_state(database: &Database) -> anyhow::Result<()> { 12 | println!("Data in column family state:"); 13 | let mut overview = vec![]; 14 | for entry in database.iterator_cf("state").unwrap() { 15 | let (key, value) = entry?; 16 | let key_str = str::from_utf8(&key)?; 17 | let value_str = str::from_utf8(&value)?; 18 | overview.push(StateInfo { 19 | twitter_username: key_str.into(), 20 | last_tweet_datetime: value_str.into(), 21 | }); 22 | } 23 | println!("{}", Table::new(overview)); 24 | Ok(()) 25 | } 26 | 27 | fn display_timeline(database: &Database) -> anyhow::Result<()> { 28 | // Start with en empty line. 29 | println!("\nData in column family timeline:"); 30 | let timeline = database.iterator_cf("timeline").unwrap(); 31 | for entry in timeline { 32 | let (key, value) = entry?; 33 | let key_str = str::from_utf8(&key)?; 34 | let value_str: Tweet = serde_json::from_slice(&value)?; 35 | println!(" {key_str} = {value_str:?}"); 36 | } 37 | Ok(()) 38 | } 39 | 40 | #[derive(Tabled)] 41 | struct StateInfo { 42 | twitter_username: String, 43 | last_tweet_datetime: String, 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use reqwest::Client; 49 | use rocksdb::{Options, DB}; 50 | 51 | use super::info; 52 | use crate::{commands::Poll, config::PollConfig, database::Database}; 53 | 54 | // To test this function: 55 | // RUST_LOG=debug cargo test get_info -- --ignored --show-output '[auth_token]' 56 | #[test_log::test(tokio::test)] 57 | #[ignore = "require command line input"] 58 | async fn get_info() { 59 | let mut args = std::env::args().rev(); 60 | let auth_token = args.next(); 61 | 62 | let rocksdb_path = "test"; 63 | let database = Database::open(rocksdb_path); 64 | let client = Client::new(); 65 | let poll_config = vec![PollConfig { 66 | included: true, 67 | username: "TwitterDev".into(), 68 | max_results: Some(5), 69 | start_time: Some("2022-10-25T00:00:00.000Z".into()), 70 | end_time: Some("2022-10-30T00:00:00.000Z".into()), 71 | since_id: None, 72 | }]; 73 | 74 | let mut poll = Poll::new(auth_token, poll_config, &client, &database).unwrap(); 75 | poll.run().await.unwrap(); 76 | info(&database).unwrap(); 77 | 78 | drop(database); 79 | DB::destroy(&Options::default(), rocksdb_path).unwrap(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/commands/poll.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use chrono::{DateTime, Duration}; 3 | use reqwest::Client; 4 | use std::{collections::HashMap, str}; 5 | use tracing::{info, trace}; 6 | use url::Url; 7 | 8 | use crate::{ 9 | config::PollConfig, 10 | database::Database, 11 | twitter::{PaginationToken, Timeline, Tweet, UrlBuilder, Users}, 12 | }; 13 | 14 | /// Poll command entry. 15 | pub(crate) struct Poll<'a> { 16 | twitter_token: String, 17 | config: Vec, 18 | client: &'a Client, 19 | database: &'a Database, 20 | } 21 | 22 | impl<'a> Poll<'a> { 23 | pub(crate) fn new( 24 | twitter_token: Option, 25 | poll_config: Vec, 26 | client: &'a Client, 27 | database: &'a Database, 28 | ) -> Result { 29 | let twitter_token = twitter_token.ok_or_else(|| anyhow!("Empty twitter token"))?; 30 | Ok(Self { 31 | twitter_token, 32 | config: poll_config, 33 | client, 34 | database, 35 | }) 36 | } 37 | 38 | pub(crate) async fn run(&mut self) -> Result<()> { 39 | let user_map = self.user_map(self.client).await?; 40 | 41 | // Loop Twitter users in poll configs. 42 | for cfg in &mut self.config { 43 | let start_time = Self::fetch_state(self.database, &cfg.username)?; 44 | // Note: `start_time` in persistent state has higher priority than that in poll config. 45 | cfg.insert_start_time(start_time); 46 | info!("Polling timeline with config: {cfg:?}",); 47 | 48 | let endpoint = Self::endpoint(cfg, &user_map)?; 49 | // Note: `since_id` takes higher priority than `start_time` in request query parameters. 50 | let since_id = cfg.since_id.take().map(PaginationToken::TweetID); 51 | let mut timeline = Timeline::new(self.client, endpoint, &self.twitter_token, since_id); 52 | 53 | // Poll first tweet. The first tweet is the latest one in timeline. 54 | // Extract `create_at` from tweet, and upsert it to persistent state. 55 | // So we can continually poll user's timeline from last time. 56 | if let Some(tweet) = timeline.try_next().await? { 57 | Self::upsert_state(self.database, &cfg.username, &tweet.created_at)?; 58 | Self::insert_tweet(self.database, &cfg.username, &tweet)?; 59 | } 60 | // Poll remaining tweets. 61 | while let Some(tweet) = timeline.try_next().await? { 62 | Self::insert_tweet(self.database, &cfg.username, &tweet)?; 63 | } 64 | } 65 | info!("Finished polling all timeline."); 66 | Ok(()) 67 | } 68 | 69 | // Gets `create_at` of a latest tweet in persistent state, then adds one second to it 70 | // to be used as `start_time` in timeline request query. This is necessary to deduplicate 71 | // a tweet when polling. 72 | fn fetch_state(database: &Database, username: &str) -> Result> { 73 | if let Some(value) = database.get_cf("state", username)? { 74 | let value_str = str::from_utf8(&value)?; 75 | Ok(DateTime::parse_from_rfc3339(value_str)? 76 | .checked_add_signed(Duration::seconds(1)) 77 | .map(|datetime| datetime.to_rfc3339())) 78 | } else { 79 | Ok(None) 80 | } 81 | } 82 | 83 | fn upsert_state(database: &Database, username: &str, created_at: &str) -> Result<()> { 84 | trace!("Upsert state: key: {username}, value: {created_at}"); 85 | database.put_cf("state", username, created_at) 86 | } 87 | 88 | fn insert_tweet(database: &Database, username: &str, tweet: &Tweet) -> Result<()> { 89 | let key = format!("{username}:{}", tweet.id); 90 | let value = 91 | serde_json::to_vec(&tweet).with_context(|| "could not serialize tweet data to json")?; 92 | trace!("Insert tweet: key: {key}, value: {tweet:?}"); 93 | database.put_cf("timeline", key, value) 94 | } 95 | 96 | fn endpoint(config: &PollConfig, user_map: &HashMap) -> Result { 97 | // Unwrap it directly since we are sure it's not None. 98 | let user_id = user_map.get(config.username.as_str()).unwrap(); 99 | Ok(UrlBuilder::new(user_id)? 100 | .tweet_fields(vec!["created_at"]) 101 | // Set default `max_results` value: 100. 102 | .max_results(config.max_results.unwrap_or(100)) 103 | .start_time(config.start_time.as_deref()) 104 | .end_time(config.end_time.as_deref()) 105 | .build()) 106 | } 107 | 108 | /// Returns a username to user_id map. 109 | async fn user_map(&self, client: &Client) -> Result> { 110 | let usernames = self 111 | .config 112 | .iter() 113 | .map(|cfg| cfg.username.as_str()) 114 | .collect(); 115 | Users::fetch(client, usernames, &self.twitter_token) 116 | .await? 117 | .ok_or_else(|| anyhow!("No Twitter users found")) 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use reqwest::Client; 124 | use rocksdb::{Options, DB}; 125 | 126 | use super::Poll; 127 | use crate::{config::PollConfig, database::Database}; 128 | 129 | // To test this function: 130 | // RUST_LOG=debug cargo test poll -- --ignored '[auth_token]' 131 | #[test_log::test(tokio::test)] 132 | #[ignore = "require command line input"] 133 | async fn poll() { 134 | let mut args = std::env::args().rev(); 135 | let auth_token = args.next(); 136 | 137 | let rocksdb_path = "test"; 138 | let database = Database::open(rocksdb_path); 139 | let client = Client::new(); 140 | let mut poll_config = vec![PollConfig { 141 | included: true, 142 | username: "TwitterDev".into(), 143 | max_results: Some(5), 144 | start_time: Some("2022-10-25T00:00:00.000Z".into()), 145 | end_time: Some("2022-10-30T00:00:00.000Z".into()), 146 | since_id: None, 147 | }]; 148 | { 149 | let mut poll = 150 | Poll::new(auth_token.clone(), poll_config.clone(), &client, &database).unwrap(); 151 | poll.run().await.unwrap(); 152 | } 153 | { 154 | // Remove `start_time` and `end_time` fields. 155 | poll_config.iter_mut().for_each(|cfg| { 156 | cfg.start_time.take(); 157 | cfg.end_time.replace("2022-12-01T00:00:00.000Z".into()); 158 | }); 159 | let mut poll = Poll::new(auth_token, poll_config, &client, &database).unwrap(); 160 | // Poll again from last time. 161 | poll.run().await.unwrap(); 162 | } 163 | 164 | drop(database); 165 | DB::destroy(&Options::default(), rocksdb_path).unwrap(); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/commands/push.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Context, Result}; 2 | use reqwest::{Client, StatusCode}; 3 | use std::{collections::HashMap, str, time::Duration}; 4 | use tokio::{ 5 | signal::{ 6 | self, 7 | unix::{signal as unix_signal, SignalKind}, 8 | }, 9 | sync::oneshot::{self, Receiver}, 10 | time, 11 | }; 12 | use tracing::{debug, info, warn}; 13 | 14 | use crate::{config::PushConfig, database::Database, telegram::Message, twitter::Tweet}; 15 | 16 | /// Push command entry. 17 | /// 18 | /// The `first_entry` and `last_entry` fields are used to mark 19 | /// entries range in timeline column family that successfully 20 | /// pushed to Telegram channel(s). So that we can delete them 21 | /// from database after pushing. 22 | pub(crate) struct Push<'a> { 23 | telegram_token: String, 24 | config: Vec, 25 | client: &'a Client, 26 | database: &'a mut Database, 27 | /// The first entry when reading timeline column family for pushing. 28 | first_entry: Option>, 29 | /// The last entry when reading timeline column family for pushing. 30 | last_entry: Option>, 31 | /// Shutdown signal. 32 | signal: Receiver<()>, 33 | } 34 | 35 | impl<'a> Push<'a> { 36 | pub(crate) fn new( 37 | telegram_token: Option, 38 | config: Vec, 39 | client: &'a Client, 40 | database: &'a mut Database, 41 | ) -> Result { 42 | let telegram_token = telegram_token.ok_or_else(|| anyhow!("Empty Telegram token"))?; 43 | let signal = shutdown_signal(); 44 | Ok(Self { 45 | telegram_token, 46 | config, 47 | client, 48 | database, 49 | first_entry: None, 50 | last_entry: None, 51 | signal, 52 | }) 53 | } 54 | 55 | pub(crate) async fn run(&mut self) -> Result<()> { 56 | let user_map = self.user_map(); 57 | // Read timeline column family from database. 58 | // Note: we're sure there's a timeline iterator, so just unwrap it directly. 59 | for (i, entry) in self.database.iterator_cf("timeline").unwrap().enumerate() { 60 | let (key, value) = entry?; 61 | if i == 0 { 62 | // Keep the first entry key. 63 | self.first_entry = Some(key.clone()); 64 | } 65 | 66 | // Check shutdown signal first. 67 | if self.signal.try_recv().is_ok() { 68 | self.last_entry = Some(key); 69 | break; 70 | } 71 | 72 | let (twitter_username, tweet) = { 73 | let key_str = str::from_utf8(&key)?; 74 | let tweet: Tweet = serde_json::from_slice(&value)?; 75 | // Unwrap it directly since we're sure it's Some(&str). 76 | let (twitter_username, _) = key_str.split_once(':').unwrap(); 77 | (twitter_username, tweet) 78 | }; 79 | debug!("Read {twitter_username}'s tweet."); 80 | if let Some(telegram_channel) = user_map.get(twitter_username) { 81 | debug!("Push tweet to {telegram_channel}"); 82 | let response = Message::new(telegram_channel, tweet) 83 | .send(self.client, &self.telegram_token) 84 | .await 85 | .with_context(|| "Failed to send message to Telegram channel") 86 | .map_err(|err| { 87 | // This error check is necessary in order to tidy database despite error or panic. 88 | self.last_entry = Some(key.clone()); 89 | err 90 | })?; 91 | match response.status() { 92 | // Note: Telegram bot api applies requests rate limit. 93 | StatusCode::OK => time::sleep(Duration::from_secs(3)).await, 94 | other => { 95 | warn!( 96 | "Request not successful, channel: {telegram_channel}, response status: {other}, body: {}", 97 | response.text().await.unwrap_or_else(|_| "".to_string()) 98 | ); 99 | // Keep the last entry key. 100 | self.last_entry = Some(key); 101 | break; 102 | } 103 | } 104 | } 105 | } 106 | Ok(()) 107 | } 108 | 109 | fn tidy_database(&mut self) -> Result<()> { 110 | match (self.first_entry.take(), self.last_entry.take()) { 111 | (Some(first_entry), Some(last_entry)) => { 112 | info!("Push stopped, deleting pushed tweets in database."); 113 | self.database 114 | .delete_range_cf("timeline", first_entry, last_entry) 115 | } 116 | (Some(_), None) => { 117 | info!("Finished pushing all timeline."); 118 | self.database.drop_cf("timeline") 119 | } 120 | _ => { 121 | info!("No tweets to push."); 122 | Ok(()) 123 | } 124 | } 125 | } 126 | 127 | /// Returns a Twitter username to Telegram channel map. 128 | fn user_map(&mut self) -> HashMap { 129 | self.config 130 | .drain(..) 131 | .map(|cfg| (cfg.from, cfg.username)) 132 | .collect() 133 | } 134 | } 135 | 136 | impl<'a> Drop for Push<'a> { 137 | fn drop(&mut self) { 138 | let _ = self.tidy_database(); 139 | } 140 | } 141 | 142 | /// Handles user shutdown signals. 143 | fn shutdown_signal() -> Receiver<()> { 144 | let (tx, rx) = oneshot::channel(); 145 | tokio::spawn(async move { 146 | let mut terminate_stream = 147 | unix_signal(SignalKind::terminate()).expect("failed to listen for event"); 148 | let mut hangup_stream = 149 | unix_signal(SignalKind::hangup()).expect("failed to listen for event"); 150 | let mut quit_stream = unix_signal(SignalKind::quit()).expect("failed to listen for event"); 151 | tokio::select! { 152 | completion = signal::ctrl_c() => { 153 | completion.expect("failed to listen for event"); 154 | info!("Received ctrl-c signal."); 155 | }, 156 | _ = terminate_stream.recv()=> info!("Received SIGTERM signal."), 157 | _ = hangup_stream.recv()=> info!("Received SIGHUP signal."), 158 | _ = quit_stream.recv()=> info!("Received SIGQUIT signal."), 159 | } 160 | 161 | let _ = tx.send(()); 162 | }); 163 | rx 164 | } 165 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::path::PathBuf; 3 | 4 | #[derive(Deserialize, Debug)] 5 | pub struct Config { 6 | pub(crate) rocksdb_path: PathBuf, 7 | pub(crate) twitter_token: Option, 8 | pub(crate) telegram_token: Option, 9 | pub(crate) poll: Option>, 10 | pub(crate) push: Option>, 11 | } 12 | 13 | #[derive(Deserialize, Debug, Clone)] 14 | pub(crate) struct PollConfig { 15 | pub(crate) included: bool, 16 | pub(crate) username: String, 17 | pub(crate) max_results: Option, 18 | pub(crate) start_time: Option, 19 | pub(crate) end_time: Option, 20 | pub(crate) since_id: Option, 21 | } 22 | 23 | #[derive(Deserialize, Debug)] 24 | pub(crate) struct PushConfig { 25 | pub(crate) included: bool, 26 | pub(crate) from: String, 27 | pub(crate) username: String, 28 | } 29 | 30 | impl PollConfig { 31 | pub(crate) fn insert_start_time(&mut self, start_time: Option) { 32 | start_time.map(|start_time| self.start_time.insert(start_time)); 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::Config; 39 | 40 | #[test] 41 | fn decode() { 42 | let toml_str = r#" 43 | rocksdb_path = "rocksdb" 44 | twitter_token = "xxx" 45 | telegram_token = "xxx" 46 | 47 | [[poll]] 48 | included = true 49 | username = "TwitterDev" 50 | max_results = 5 51 | start_time = "2022-10-25T00:00:00.000Z" 52 | end_time = "2022-11-01T00:00:00.000Z" 53 | since_id = "xyz" 54 | 55 | [[push]] 56 | included = true 57 | from = "TwitterDev" 58 | username = "some_bot" 59 | "#; 60 | let decoded = toml::from_str::(toml_str); 61 | assert!(decoded.is_ok()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{bail, Context, Result}; 2 | use rocksdb::{ColumnFamilyDescriptor, DBIterator, IteratorMode, Options, DB}; 3 | use std::path::Path; 4 | 5 | #[derive(Debug)] 6 | pub(crate) struct Database(DB); 7 | 8 | impl Database { 9 | pub(crate) fn open>(path: P) -> Self { 10 | let cfds: Vec = vec!["timeline", "state"] 11 | .iter() 12 | .map(|&cf| ColumnFamilyDescriptor::new(cf, Options::default())) 13 | .collect(); 14 | 15 | let mut db_opts = Options::default(); 16 | db_opts.create_if_missing(true); 17 | db_opts.create_missing_column_families(true); 18 | 19 | let db = DB::open_cf_descriptors(&db_opts, path, cfds).expect("could not open rocksdb"); 20 | Self(db) 21 | } 22 | 23 | pub(crate) fn put_cf(&self, cf: &str, key: K, value: V) -> Result<()> 24 | where 25 | K: AsRef<[u8]>, 26 | V: AsRef<[u8]>, 27 | { 28 | match self.0.cf_handle(cf) { 29 | Some(cf_handle) => Ok(self.0.put_cf(cf_handle, key, value)?), 30 | None => bail!("no such column family: {cf}"), 31 | } 32 | } 33 | 34 | pub(crate) fn get_cf>(&self, cf: &str, key: K) -> Result>> { 35 | match self.0.cf_handle(cf) { 36 | Some(cf_handle) => self 37 | .0 38 | .get_cf(cf_handle, key) 39 | .with_context(|| "could not get value from column family"), 40 | None => bail!("no such column family: {cf}"), 41 | } 42 | } 43 | 44 | pub(crate) fn iterator_cf(&self, cf: &str) -> Option { 45 | self.0 46 | .cf_handle(cf) 47 | .map(|cf_handle| self.0.iterator_cf(cf_handle, IteratorMode::Start)) 48 | } 49 | 50 | /// Performs an `from` inclusive but `to` exclusive range (`["from", "to")`) deletion. 51 | pub(crate) fn delete_range_cf(&self, cf: &str, from: K, to: K) -> Result<()> 52 | where 53 | K: AsRef<[u8]>, 54 | { 55 | match self.0.cf_handle(cf) { 56 | Some(cf_handle) => Ok(self.0.delete_range_cf(cf_handle, from, to)?), 57 | None => bail!("no such column family: {cf}"), 58 | } 59 | } 60 | 61 | pub(crate) fn drop_cf(&mut self, cf: &str) -> Result<()> { 62 | Ok(self.0.drop_cf(cf)?) 63 | } 64 | } 65 | 66 | #[cfg(test)] 67 | mod tests { 68 | use super::Database; 69 | use rocksdb::{Options, DB}; 70 | 71 | #[test] 72 | fn open() { 73 | let db = Database::open("test"); 74 | drop(db); 75 | DB::destroy(&Options::default(), "test").unwrap(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod app; 2 | mod commands; 3 | mod config; 4 | mod database; 5 | mod telegram; 6 | mod twitter; 7 | 8 | pub use app::App; 9 | pub use config::Config; 10 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use pigeon::{App, Config}; 3 | use std::path::PathBuf; 4 | use tokio::{fs::File, io::AsyncReadExt}; 5 | use tracing_subscriber::{ 6 | prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, 7 | }; 8 | 9 | #[derive(Parser, Debug)] 10 | struct Cli { 11 | /// Activate debug mode 12 | #[arg(short, long, action)] 13 | debug: bool, 14 | 15 | /// Config file path 16 | #[arg(short, long, default_value = "config.toml", value_name = "config.toml")] 17 | config_path: PathBuf, 18 | 19 | #[command(subcommand)] 20 | command: Command, 21 | } 22 | 23 | #[derive(Subcommand, Debug)] 24 | enum Command { 25 | /// Poll Twitter users' timeline 26 | Poll, 27 | /// Push timeline to Telegram channel(s) 28 | Push, 29 | /// Display overview information from Database 30 | Info, 31 | } 32 | 33 | #[tokio::main] 34 | async fn main() -> anyhow::Result<()> { 35 | let cli = Cli::parse(); 36 | match cli.debug { 37 | true => setup_logging("debug"), 38 | false => setup_logging("info"), 39 | } 40 | let config = load_config(cli.config_path).await?; 41 | let mut app = App::new(config); 42 | match cli.command { 43 | Command::Poll => app.poll().await?, 44 | Command::Push => app.push().await?, 45 | Command::Info => app.info()?, 46 | } 47 | Ok(()) 48 | } 49 | 50 | async fn load_config(path: PathBuf) -> anyhow::Result { 51 | let mut file = File::open(path).await?; 52 | let mut buf = String::new(); 53 | file.read_to_string(&mut buf).await?; 54 | 55 | let config: Config = toml::from_str(&buf)?; 56 | Ok(config) 57 | } 58 | 59 | fn setup_logging(level: &str) { 60 | let fmt_layer = tracing_subscriber::fmt::Layer::default(); 61 | let filter_layer = EnvFilter::try_from_default_env() 62 | .or_else(|_| EnvFilter::try_new(level)) 63 | .unwrap(); 64 | tracing_subscriber::registry() 65 | .with(fmt_layer) 66 | .with(filter_layer) 67 | .init(); 68 | } 69 | -------------------------------------------------------------------------------- /src/telegram.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use reqwest::{Client, Response}; 3 | use serde::Serialize; 4 | use url::Url; 5 | 6 | use crate::twitter::Tweet; 7 | 8 | /// A message sent by Telegram bot. 9 | #[derive(Debug, Serialize)] 10 | pub(crate) struct Message { 11 | /// Telegram channel username. 12 | chat_id: String, 13 | /// Message text body. 14 | text: String, 15 | } 16 | 17 | impl Message { 18 | pub(crate) fn new(channel: &str, tweet: Tweet) -> Self { 19 | Self { 20 | chat_id: channel.into(), 21 | text: format!("{}\n\n{}", tweet.text, tweet.created_at), 22 | } 23 | } 24 | 25 | pub(crate) async fn send(&self, client: &Client, telegram_token: &str) -> Result { 26 | Ok(client 27 | .post(endpoint(telegram_token)?) 28 | .json(self) 29 | .send() 30 | .await?) 31 | } 32 | } 33 | 34 | /// An endpoint for sending messages by Telegram bot. 35 | /// See: https://core.telegram.org/bots/api#sendmessage 36 | fn endpoint(token: &str) -> Result { 37 | let api = Url::parse("https://api.telegram.org/") 38 | .with_context(|| "Could not parse Telegram api base endpoint")?; 39 | Url::options() 40 | .base_url(Some(&api)) 41 | .parse(format!("/bot{token}/sendMessage").as_str()) 42 | .with_context(|| "could not parse Telegram api path") 43 | } 44 | -------------------------------------------------------------------------------- /src/twitter.rs: -------------------------------------------------------------------------------- 1 | mod timeline; 2 | mod users; 3 | 4 | pub(crate) use users::Users; 5 | pub(crate) use timeline::{Timeline, UrlBuilder, PaginationToken, Data as Tweet}; 6 | 7 | const API_ENDPOINT_BASE: &str = "https://api.twitter.com/2/"; 8 | -------------------------------------------------------------------------------- /src/twitter/timeline.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use reqwest::Client; 3 | use reqwest::StatusCode; 4 | use serde::{Deserialize, Serialize}; 5 | use tracing::{info, trace, warn}; 6 | use url::Url; 7 | 8 | use super::API_ENDPOINT_BASE; 9 | 10 | /// Timeline continually yields all tweets in timeline which may be paginated. 11 | pub(crate) struct Timeline<'a> { 12 | client: &'a Client, 13 | url: Url, 14 | auth_token: &'a str, 15 | pagination_token: Option, 16 | page: u8, 17 | texts: as IntoIterator>::IntoIter, 18 | } 19 | 20 | #[derive(Debug)] 21 | pub(crate) enum PaginationToken { 22 | NextToken(String), 23 | TweetID(String), 24 | } 25 | 26 | impl<'a> Timeline<'a> { 27 | pub(crate) fn new( 28 | client: &'a Client, 29 | url: Url, 30 | auth_token: &'a str, 31 | pagination_token: Option, 32 | ) -> Self { 33 | Self { 34 | client, 35 | url, 36 | auth_token, 37 | pagination_token, 38 | page: 0, 39 | texts: vec![].into_iter(), 40 | } 41 | } 42 | 43 | pub(crate) async fn try_next(&mut self) -> Result> { 44 | if let Some(text) = self.texts.next() { 45 | return Ok(Some(text)); 46 | } 47 | 48 | // Check if pagination token is present. 49 | let url = match self.pagination_token.take() { 50 | Some(pagination_token) => self.url_with_pagination(pagination_token), 51 | None => match self.page { 52 | // The first request. 53 | 0 => self.url.clone(), 54 | // The last request. 55 | n => { 56 | info!("Finished polling timeline, total pages: {n}"); 57 | return Ok(None); 58 | } 59 | }, 60 | }; 61 | 62 | let response = self 63 | .client 64 | .get(url) 65 | .bearer_auth(self.auth_token) 66 | .send() 67 | .await 68 | .with_context(|| "Failed to request timeline")?; 69 | // Check response status. 70 | match response.status() { 71 | StatusCode::OK => { 72 | let timeline: Tweets = response 73 | .json() 74 | .await 75 | .with_context(|| "Failed to deserialize json response")?; 76 | trace!(?timeline); 77 | 78 | // Keep the pagination token for next request. 79 | self.pagination_token = timeline 80 | .meta 81 | .and_then(|mut meta| meta.next_token.take().map(PaginationToken::NextToken)); 82 | 83 | // Increase page number on request success. 84 | match timeline.data { 85 | Some(tweets) => { 86 | self.page += 1; 87 | self.texts = tweets.into_iter(); 88 | Ok(self.texts.next()) 89 | } 90 | // In a case that "start_time" query parameter is specified in timeline request, 91 | // "next_token" is always returned in the last page metadata. To avoid endless unnecessary 92 | // page requests, we exit immediately here. 93 | None => Ok(None), 94 | } 95 | } 96 | StatusCode::TOO_MANY_REQUESTS => { 97 | info!( 98 | "twitter timeline endpoint rate limit reached, please wait for at least 15 mins before next try: {}", 99 | response.status()); 100 | Ok(None) 101 | } 102 | x => { 103 | warn!( 104 | result = "request not successful", 105 | status = %x, 106 | body = response.text().await.unwrap_or_else(|_| "".to_string()), 107 | query = self.url.query(), 108 | page = self.page, 109 | ); 110 | Ok(None) 111 | } 112 | } 113 | } 114 | 115 | /// We don't mutate original `Url`, we return a clone one since `Url.append_pair` will append duplicated key value pairs. 116 | fn url_with_pagination(&self, pagination_token: PaginationToken) -> Url { 117 | let mut url = self.url.clone(); 118 | match pagination_token { 119 | PaginationToken::NextToken(next_token) => url 120 | .query_pairs_mut() 121 | .append_pair("pagination_token", &next_token), 122 | PaginationToken::TweetID(tweet_id) => { 123 | url.query_pairs_mut().append_pair("since_id", &tweet_id) 124 | } 125 | }; 126 | url 127 | } 128 | } 129 | 130 | /// Response from Twitter timeline api. 131 | #[derive(Debug, Deserialize, Clone)] 132 | pub(crate) struct Tweets { 133 | data: Option>, 134 | meta: Option, 135 | } 136 | 137 | #[derive(Debug, Deserialize, Serialize, Clone)] 138 | pub(crate) struct Data { 139 | pub(crate) id: String, 140 | pub(crate) created_at: String, 141 | pub(crate) text: String, 142 | } 143 | 144 | #[derive(Debug, Deserialize, Clone)] 145 | struct Meta { 146 | oldest_id: Option, 147 | newest_id: Option, 148 | result_count: Option, 149 | next_token: Option, 150 | } 151 | 152 | // Builds a Twitter user timeline endpoint URL. 153 | #[derive(Debug, Clone)] 154 | pub(crate) struct UrlBuilder(Url); 155 | 156 | impl UrlBuilder { 157 | pub(crate) fn new(user_id: &str) -> Result { 158 | let base_url = Url::parse(API_ENDPOINT_BASE).unwrap(); 159 | Url::options() 160 | .base_url(Some(&base_url)) 161 | .parse(format!("users/{user_id}/tweets").as_str()) 162 | .map(Self) 163 | .with_context(|| "Failed to parse url from user_id segment") 164 | } 165 | 166 | pub(crate) fn tweet_fields(mut self, tweet_fields: Vec<&str>) -> Self { 167 | self.0 168 | .query_pairs_mut() 169 | .append_pair("tweet.fields", &tweet_fields.join(",")); 170 | self 171 | } 172 | 173 | pub(crate) fn max_results(mut self, max_results: u8) -> Self { 174 | self.0 175 | .query_pairs_mut() 176 | .append_pair("max_results", &max_results.to_string()); 177 | self 178 | } 179 | 180 | /// String format for `start_time` is RFC3339, for example, "2020-12-12T01:00:00Z". 181 | pub(crate) fn start_time(mut self, start_time: Option<&str>) -> Self { 182 | if let Some(start_time) = start_time { 183 | self.0 184 | .query_pairs_mut() 185 | .append_pair("start_time", start_time); 186 | } 187 | self 188 | } 189 | 190 | /// String format for `end_time` is RFC3339, for example, "2020-12-12T01:00:00Z". 191 | pub(crate) fn end_time(mut self, end_time: Option<&str>) -> Self { 192 | if let Some(end_time) = end_time { 193 | self.0.query_pairs_mut().append_pair("end_time", end_time); 194 | } 195 | self 196 | } 197 | 198 | pub(crate) fn build(self) -> Url { 199 | self.0 200 | } 201 | } 202 | 203 | #[cfg(test)] 204 | mod tests { 205 | use reqwest::Client; 206 | use tracing::debug; 207 | 208 | use super::{PaginationToken, Timeline, Tweets, UrlBuilder, API_ENDPOINT_BASE}; 209 | 210 | #[test] 211 | fn build_url() { 212 | let url = UrlBuilder::new("123").unwrap().build(); 213 | assert_eq!( 214 | format!("{API_ENDPOINT_BASE}users/{}/tweets", "123"), 215 | url.as_str() 216 | ); 217 | } 218 | 219 | #[test] 220 | fn url_queries() { 221 | let url = UrlBuilder::new("") 222 | .unwrap() 223 | .tweet_fields(vec!["created_at"]) 224 | .max_results(100) 225 | .start_time(Some("2022-11-21T12:23:43.812Z")) 226 | .end_time(Some("2022-11-24T12:23:43.812Z")) 227 | .build(); 228 | assert_eq!( 229 | "tweet.fields=created_at&max_results=100&start_time=2022-11-21T12%3A23%3A43.812Z&end_time=2022-11-24T12%3A23%3A43.812Z", 230 | url.query().unwrap() 231 | ); 232 | } 233 | 234 | #[test] 235 | fn parse_timeline() { 236 | let timeline_data = r#" 237 | { 238 | "data": [ 239 | { 240 | "created_at": "2022-11-02T23:15:29.000Z", 241 | "text": "As always, we’re just a Tweet away, so feel free to reach out with any questions. We’re grateful for your partnership to #BuildWhatsNext", 242 | "id": "1587946527955329024", 243 | "edit_history_tweet_ids": [ 244 | "1587946527955329024" 245 | ] 246 | }, 247 | { 248 | "created_at": "2022-11-02T23:15:29.000Z", 249 | "text": "We’ll still celebrate the soon-to-be-announced winners of our Chirp Developer Challenge - stay tuned for more details!", 250 | "id": "1587946526617264128", 251 | "edit_history_tweet_ids": [ 252 | "1587946526617264128" 253 | ] 254 | }, 255 | { 256 | "created_at": "2022-11-02T23:15:28.000Z", 257 | "text": "We’re currently hard at work to make Twitter better for everyone, including developers! We’ve decided to cancel the #Chirp developer conference while we build some things that we’re excited to share with you soon.", 258 | "id": "1587946525245816832", 259 | "edit_history_tweet_ids": [ 260 | "1587946525245816832" 261 | ] 262 | }, 263 | { 264 | "created_at": "2022-11-01T19:00:00.000Z", 265 | "text": "💡 #TipTuesday: Ever wondered how to get the video URL from a Tweet in Twitter API v2? 👀 Here’s a walkthrough, using our TypeScript SDK. 💫\n\nhttps://t.co/tFQ4Eskq7t", 266 | "id": "1587519847281397767", 267 | "edit_history_tweet_ids": [ 268 | "1587519847281397767" 269 | ] 270 | }, 271 | { 272 | "created_at": "2022-10-31T13:00:01.000Z", 273 | "text": "✍️Fill in the blank ⬇️\n\nI start my morning off by _____", 274 | "id": "1587066866824085505", 275 | "edit_history_tweet_ids": [ 276 | "1587066866824085505" 277 | ] 278 | } 279 | ], 280 | "meta": { 281 | "result_count": 5, 282 | "newest_id": "1587946527955329024", 283 | "oldest_id": "1587066866824085505", 284 | "next_token": "7140dibdnow9c7btw423x78o50g6e358t5r7iusluud6d" 285 | } 286 | }"#; 287 | 288 | serde_json::from_str::(timeline_data).unwrap(); 289 | } 290 | 291 | // To test this function: 292 | // RUST_LOG=debug cargo test tweets -- --ignored '[auth_token]' 293 | #[test_log::test(tokio::test)] 294 | #[ignore = "require command line input"] 295 | async fn tweets() { 296 | let mut args = std::env::args().rev(); 297 | let auth_token = args.next().unwrap(); 298 | 299 | let client = Client::new(); 300 | let endpoint = UrlBuilder::new("2244994945") 301 | .unwrap() 302 | .tweet_fields(vec!["created_at"]) 303 | .max_results(10) 304 | .start_time(Some("2022-10-25T00:00:00.000Z")) 305 | .end_time(Some("2022-11-04T00:00:00.000Z")) 306 | .build(); 307 | { 308 | debug!("Timeline without pagination token"); 309 | let mut timeline = Timeline::new(&client, endpoint.clone(), &auth_token, None); 310 | 311 | while let Some(tweet) = timeline.try_next().await.unwrap() { 312 | debug!(?tweet); 313 | } 314 | } 315 | { 316 | debug!("Timeline with pagination token"); 317 | let tweet_id = PaginationToken::TweetID("1586025008899448832".into()); 318 | let mut timeline = Timeline::new(&client, endpoint, &auth_token, Some(tweet_id)); 319 | while let Some(tweet) = timeline.try_next().await.unwrap() { 320 | debug!(?tweet); 321 | } 322 | } 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /src/twitter/users.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use reqwest::Client; 3 | use serde::Deserialize; 4 | use std::collections::HashMap; 5 | use tracing::warn; 6 | use url::Url; 7 | 8 | use super::API_ENDPOINT_BASE; 9 | 10 | /// Response from Twitter users lookup api. 11 | #[derive(Debug, Deserialize)] 12 | pub(crate) struct Users { 13 | data: Option>, 14 | errors: Option>, 15 | } 16 | 17 | #[derive(Debug, Deserialize)] 18 | struct Data { 19 | id: String, 20 | name: String, 21 | username: String, 22 | } 23 | 24 | #[derive(Debug, Deserialize)] 25 | struct Error { 26 | value: String, 27 | detail: String, 28 | title: String, 29 | resource_type: String, 30 | parameter: String, 31 | resource_id: String, 32 | #[serde(rename(deserialize = "type"))] 33 | typ: String, 34 | } 35 | 36 | impl Users { 37 | /// Fetch users to return a username to user_id map. 38 | pub(crate) async fn fetch( 39 | client: &Client, 40 | usernames: Vec<&str>, 41 | auth_token: &str, 42 | ) -> Result>> { 43 | let endpoint = Self::endpoint(usernames)?; 44 | Self::send_request(client, endpoint, auth_token).await 45 | } 46 | 47 | fn endpoint(usernames: Vec<&str>) -> Result { 48 | let base_url = Url::parse(API_ENDPOINT_BASE).unwrap(); 49 | let usernames = usernames.join(","); 50 | let mut url = Url::options() 51 | .base_url(Some(&base_url)) 52 | .parse("users/by") 53 | .with_context(|| "Failed to parse users look up endpoint")?; 54 | url.set_query(Some(format!("usernames={usernames}").as_str())); 55 | 56 | Ok(url) 57 | } 58 | 59 | async fn send_request( 60 | client: &Client, 61 | endpoint: Url, 62 | auth_token: &str, 63 | ) -> Result>> { 64 | let response = client 65 | .get(endpoint) 66 | .bearer_auth(auth_token) 67 | .send() 68 | .await 69 | .with_context(|| "Request failed to get users")?; 70 | if !response.status().is_success() { 71 | warn!( 72 | "request not successful, got response status: {}", 73 | response.status() 74 | ); 75 | return Ok(None); 76 | } 77 | 78 | let users: Users = response 79 | .json() 80 | .await 81 | .with_context(|| "Failed to deserialize json response")?; 82 | if users.errors.is_some() { 83 | warn!( 84 | "Errors occurred when requesting users: {:#?}", 85 | users.errors.unwrap() 86 | ); 87 | } 88 | if let Some(users) = users.data { 89 | let user_ids = users 90 | .into_iter() 91 | .map(|data| (data.username, data.id)) 92 | .collect(); 93 | Ok(Some(user_ids)) 94 | } else { 95 | Ok(None) 96 | } 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use reqwest::Client; 103 | use serde_json::Result; 104 | use tracing::debug; 105 | 106 | use super::Users; 107 | use crate::twitter::API_ENDPOINT_BASE; 108 | 109 | #[test] 110 | fn endpoint() { 111 | let usernames = vec!["john", "mick"]; 112 | let endpoint = Users::endpoint(usernames).unwrap(); 113 | assert_eq!( 114 | format!("{API_ENDPOINT_BASE}users/by?usernames={}", "john,mick"), 115 | endpoint.as_str() 116 | ); 117 | } 118 | 119 | #[test] 120 | fn parse_users() -> Result<()> { 121 | let users_data = r#" 122 | { 123 | "data": [ 124 | { 125 | "id": "2244994945", 126 | "name": "Twitter Dev", 127 | "username": "TwitterDev" 128 | } 129 | ], 130 | "errors": [ 131 | { 132 | "value": "xn47mzh437", 133 | "detail": "Could not find user with usernames: [xn47mzh437].", 134 | "title": "Not Found Error", 135 | "resource_type": "user", 136 | "parameter": "usernames", 137 | "resource_id": "xn47mzh437", 138 | "type": "https://api.twitter.com/2/problems/resource-not-found" 139 | } 140 | ] 141 | } 142 | "#; 143 | 144 | let users: Users = serde_json::from_str(users_data)?; 145 | assert_eq!("2244994945", users.data.unwrap()[0].id); 146 | Ok(()) 147 | } 148 | 149 | // To test this function: 150 | // RUST_LOG=debug cargo test fetch -- --ignored '[auth_token]' TwitterDev,jack,1ws23x 151 | #[test_log::test(tokio::test)] 152 | #[ignore = "require command line input"] 153 | async fn fetch() { 154 | let mut args = std::env::args().rev(); 155 | let arg = args.next().unwrap(); 156 | let usernames = arg.split(',').collect(); 157 | let auth_token = args.next().unwrap(); 158 | 159 | let client = Client::new(); 160 | let users = Users::fetch(&client, usernames, auth_token.as_str()) 161 | .await 162 | .unwrap(); 163 | if let Some(users) = users { 164 | debug!(?users); 165 | } 166 | } 167 | } 168 | --------------------------------------------------------------------------------