diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index cf1edea..0000000 --- a/.dockerignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/ -.git/ - -.env -.env.* -!.env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index bfffe34..0000000 --- a/.env.example +++ /dev/null @@ -1,2 +0,0 @@ -DISCORD_TOKEN= -SAY_LOGS_CHANNEL= diff --git a/.envrc b/.envrc index a96880d..65f365c 100644 --- a/.envrc +++ b/.envrc @@ -1,2 +1,5 @@ -use flake +if has nix_direnv_version; then + use flake ./nix/dev +fi + dotenv_if_exists diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index b947077..0000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -node_modules/ -dist/ diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index 11db6be..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-env node */ - -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], - rules: { - '@typescript-eslint/no-non-null-assertion': 0, - }, -}; diff --git a/.github/workflows/autobot.yaml b/.github/workflows/autobot.yaml new file mode 100644 index 0000000..195eabe --- /dev/null +++ b/.github/workflows/autobot.yaml @@ -0,0 +1,28 @@ +name: Auto-merge Dependabot + +on: pull_request + +jobs: + automerge: + name: Check and merge PR + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + if: github.actor == 'dependabot[bot]' + + steps: + - uses: dependabot/fetch-metadata@v2 + id: metadata + with: + github-token: ${{ github.token }} + + # auto merge minor releases + - name: Enable auto-merge + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + run: gh pr merge --auto --rebase "$PR" + env: + GH_TOKEN: ${{ github.token }} + PR: ${{ github.event.pull_request.html_url }} diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..09bab7e --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,68 @@ +name: Check + +on: + push: + branches: ['main'] + pull_request: + +jobs: + rustfmt: + name: Run rustfmt + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Run rustfmt + run: cargo fmt --all -- --check + + clippy: + name: Run Clippy scan + runs-on: ubuntu-latest + + permissions: + security-events: write + + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: clippy + + - name: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install SARIF tools + run: cargo install clippy-sarif sarif-fmt + + - name: Fetch Cargo deps + run: cargo fetch --locked + + - name: Run Clippy + continue-on-error: true + run: | + set -euo pipefail + + cargo clippy \ + --all-features \ + --all-targets \ + --message-format=json \ + | clippy-sarif | tee /tmp/clippy.sarif | sarif-fmt + + - name: Upload results + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: /tmp/clippy.sarif + wait-for-processing: true diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index eab8a0b..2d4748d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,112 +3,88 @@ name: Docker on: push: branches: ['main'] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: prismlauncher/refraction - -permissions: - contents: read - packages: write + pull_request: + workflow_dispatch: jobs: build: + name: Build image runs-on: ubuntu-latest + strategy: matrix: - platform: - - linux/arm64 - - linux/amd64 + arch: [x86_64, aarch64] steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - if: ${{ matrix.platform != 'linux/amd64' }} + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v10 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v4 - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=raw,value=latest - - - name: Build and push by digest - uses: docker/build-push-action@v5 + - name: Build Docker image id: build - with: - context: . - provenance: false - labels: ${{ steps.meta.outputs.labels }} - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true - - - name: Export digests run: | - mkdir -p /tmp/digests - digest="${{ steps.build.outputs.digest }}" - touch "/tmp/digests/${digest#sha256:}" + nix build --print-build-logs ./nix/dev#container-${{ matrix.arch }} + [ ! -L result ] && exit 1 + echo "path=$(readlink -f result)" >> "$GITHUB_OUTPUT" - - name: Upload digests - uses: actions/upload-artifact@v3 + - name: Upload image + uses: actions/upload-artifact@v4 with: - name: digests - path: /tmp/digests/* + name: container-${{ matrix.arch }} + path: ${{ steps.build.outputs.path }} if-no-files-found: error - retention-days: 1 + retention-days: 3 push: + name: Push image + + needs: build runs-on: ubuntu-latest - needs: - - build + + permissions: + packages: write + + env: + REGISTRY: ghcr.io + USERNAME: ${{ github.actor }} + IMAGE_NAME: ${{ github.repository }} + + if: github.event_name == 'push' steps: - - name: Download digests - uses: actions/download-artifact@v3 + - uses: actions/checkout@v4 + + - name: Download images + uses: actions/download-artifact@v4 with: - name: digests - path: /tmp/digests + path: images - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to the Container registry + - name: Login to registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + username: ${{ env.USERNAME }} + password: ${{ github.token }} - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - tags: | - type=ref,event=branch - type=raw,value=latest - - - name: Create manifest list and push - working-directory: /tmp/digests + - name: Push to registry + env: + TAG: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *) + set -eu - - name: Inspect image - run: | - docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} + architectures=("x86_64" "aarch64") + for arch in "${architectures[@]}"; do + docker load < images/container-"$arch"/*.tar.gz + docker tag refraction:latest-"$arch" "$TAG"-"$arch" + docker push ${{ env.TAG }}-"$arch" + done + + docker manifest create "$TAG" \ + --amend "$TAG"-x86_64 \ + --amend "$TAG"-aarch64 + + docker manifest push "$TAG" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index a7a80ce..0000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Lint - -on: - push: - branches: ['main'] - pull_request: - branches: ['main'] - -jobs: - lint: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 - - name: Install Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - name: Install pnpm - uses: pnpm/action-setup@v2 - - - name: Install dependencies - run: pnpm install - - name: Lint - run: pnpm run lint diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml new file mode 100644 index 0000000..e038910 --- /dev/null +++ b/.github/workflows/nix.yml @@ -0,0 +1,53 @@ +name: Nix + +on: + push: + branches: ['main'] + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v10 + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v4 + + - name: Build refraction + run: nix build --fallback --print-build-logs + + check: + name: Check flake + + strategy: + matrix: + os: [ubuntu-latest, macos-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v10 + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@v4 + + - name: Run checks + run: | + cd ./nix/dev + nix flake check --print-build-logs --show-trace diff --git a/.github/workflows/update-flake.yml b/.github/workflows/update-flake.yml new file mode 100644 index 0000000..c9931e2 --- /dev/null +++ b/.github/workflows/update-flake.yml @@ -0,0 +1,72 @@ +name: Update flake.lock + +on: + schedule: + # run every saturday + - cron: '0 0 * * 6' + workflow_dispatch: + +jobs: + update: + name: Run update + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + env: + PR_BRANCH: 'update-lockfiles' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@v10 + + - name: Set Git user info + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + + - name: Create new branch + id: branch + run: | + git switch -c "$PR_BRANCH" + + - name: Update flake inputs + run: | + pushd nix/dev + nix flake update \ + --commit-lock-file \ + --commit-lockfile-summary "nix: update dev flake.lock" + popd + + nix flake update \ + --commit-lock-file \ + --commit-lockfile-summary "nix: update flake.lock" + + - name: Make PR if needed + env: + GH_TOKEN: ${{ github.token }} + run: | + if ! git diff --color=always --exit-code origin/main; then + git fetch origin "$PR_BRANCH" || true + git push --force-with-lease -u origin "$PR_BRANCH" + + open_prs="$(gh pr list --base main --head "$PR_BRANCH" | wc -l)" + if [ "$open_prs" -eq 0 ]; then + gh pr create \ + --base main \ + --head "$PR_BRANCH" \ + --title "chore: update lockfiles" \ + --fill + fi + fi + + - name: Enable auto-merge + shell: bash + run: gh pr merge --auto --squash + env: + GH_TOKEN: ${{ secrets.MERGE_TOKEN }} diff --git a/.gitignore b/.gitignore index ae15518..f07ccc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,32 @@ -node_modules/ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + + +# direnv secrets .env .env.* !.env.example +# Nix .direnv/ .pre-commit-config.yaml - -eslint_report.json +result* +repl-result-out* .DS_Store *.rdb + +# JetBrains +.idea/ \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f29d4e0 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2801 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +dependencies = [ + "serde", +] + +[[package]] +name = "async-trait" +version = "0.1.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "camino" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceed8ef69d8518a5dda55c07425450b58a4e1946f4951eab6d7191ee86c2443d" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.48", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "deranged" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb30d70a07a3b04884d2677f06bec33509dc67ca60d92949e5535352d3191dc" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum_dispatch" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f33313078bb8d4d05a2733a94ac4c2d8a0df9a2b84424ebf4f33bfc224a890e" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e7cf40684ae96ade6232ed84582f40ce0a66efcd43a5117aef610534f8e0b8" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-chain" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" +dependencies = [ + "version_check", +] + +[[package]] +name = "eyre" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6267a1fa6f59179ea4afc8e50fd8612a3cc60bc858f786ff877a4a8cb042799" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gray_matter" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cf2fb99fac0b821a4e61c61abff076324bb0e5c3b4a83815bbc3518a38971ad" +dependencies = [ + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.11", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" + +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http 1.1.0", + "http-body 1.0.0", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.11", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186548d73ac615b32a73aafe38fb4f56c0d340e110e5a200bcadbaf2e199263a" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.11", + "hyper 0.14.27", + "rustls 0.21.10", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.2.0", + "hyper-util", + "log", + "rustls 0.22.3", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.2.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.2.0", + "pin-project-lite", + "socket2 0.5.5", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iri-string" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21859b667d66a4c1dacd9df0863b3efb65785474255face87f5bca39dd8407c0" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ea04a7c5c055c175f189b6dc6ba036fd62306b58c66c9f6389036c503a3f4" +dependencies = [ + "base64 0.21.5", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linux-raw-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "mini-moka" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" +dependencies = [ + "crossbeam-channel", + "crossbeam-utils", + "dashmap", + "skeptic", + "smallvec", + "tagptr", + "triomphe", +] + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "octocrab" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dedddd64be5eb5ea9311d1934a09ce32ef9be22fd29990bb2a5552c17712306e" +dependencies = [ + "arc-swap", + "async-trait", + "base64 0.22.0", + "bytes", + "cfg-if", + "chrono", + "either", + "futures", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-rustls 0.26.0", + "hyper-timeout", + "hyper-util", + "jsonwebtoken", + "once_cell", + "percent-encoding", + "pin-project", + "secrecy", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "snafu", + "tokio", + "tower", + "tower-http", + "tracing", + "url", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "pem" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8fcc794035347fb64beda2d3b462595dd2753e3f268d89c5aae77e8cf2c310" +dependencies = [ + "base64 0.21.5", + "serde", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "poise" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1819d5a45e3590ef33754abce46432570c54a120798bdbf893112b4211fa09a6" +dependencies = [ + "async-trait", + "derivative", + "futures-util", + "parking_lot", + "poise_macros", + "regex", + "serenity", + "tokio", + "tracing", +] + +[[package]] +name = "poise_macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fa2c123c961e78315cd3deac7663177f12be4460f5440dbf62a7ed37b1effea" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "proc-macro2" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulldown-cmark" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998" +dependencies = [ + "bitflags 1.3.2", + "memchr", + "unicase", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redis" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d64e978fd98a0e6b105d066ba4889a7301fca65aeac850a877d8797343feeb" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.3", + "rustls-native-certs", + "rustls-pemfile 2.1.1", + "rustls-pki-types", + "ryu", + "sha1_smol", + "socket2 0.5.5", + "tokio", + "tokio-rustls 0.25.0", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "refraction" +version = "2.0.0" +dependencies = [ + "color-eyre", + "dotenvy", + "enum_dispatch", + "env_logger", + "eyre", + "gray_matter", + "log", + "octocrab", + "poise", + "redis", + "regex", + "reqwest 0.12.2", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64 0.21.5", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.11", + "http-body 0.4.6", + "hyper 0.14.27", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.10", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 0.25.3", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d66674f2b6fb864665eea7a3c1ac4e3dfacd2fda83cf6f935a612e01b0e3338" +dependencies = [ + "base64 0.21.5", + "bytes", + "futures-core", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.2.0", + "hyper-rustls 0.26.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.3", + "rustls-pemfile 1.0.4", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.1", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.2", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.1", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.5", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f48172685e6ff52a556baa527774f61fcaa884f59daf3375c62a3f1cd2549dab" +dependencies = [ + "base64 0.21.5", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "serde", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.196" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "serde_json" +version = "1.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d1bd37ce2324cf3bf85e5a25f96eb4baf0d5aa6eba43e7ae8958870c4ec48ed" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serenity" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385647faa24a889929028973650a4f158fb1b4272b2fcf94feb9fcc3c009e813" +dependencies = [ + "arrayvec", + "async-trait", + "base64 0.21.5", + "bitflags 2.4.1", + "bytes", + "chrono", + "dashmap", + "flate2", + "futures", + "fxhash", + "mime_guess", + "parking_lot", + "percent-encoding", + "reqwest 0.11.23", + "secrecy", + "serde", + "serde_json", + "time", + "tokio", + "tokio-tungstenite", + "tracing", + "typemap_rev", + "typesize", + "url", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "skeptic" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" +dependencies = [ + "bytecount", + "cargo_metadata", + "error-chain", + "glob", + "pulldown-cmark", + "tempfile", + "walkdir", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "snafu" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75976f4748ab44f6e5332102be424e7c2dc18daeaf7e725f2040c3ebb133512e" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b19911debfb8c2fb1107bc6cb2d61868aaf53a988449213959bb1b5b1ed95f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tempfile" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +dependencies = [ + "deranged", + "itoa", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +dependencies = [ + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.10", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.3", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls 0.21.10", + "tokio", + "tokio-rustls 0.24.1", + "tungstenite", + "webpki-roots 0.25.3", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.4.1", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 0.2.11", + "httparse", + "log", + "rand", + "rustls 0.21.10", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typemap_rev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74b08b0c1257381af16a5c3605254d529d3e7e109f3c62befc5d168968192998" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "typesize" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36924509726e38224322c8c90ddfbf4317324338327b7c11b7cf8672cb786da1" +dependencies = [ + "chrono", + "dashmap", + "hashbrown", + "mini-moka", + "parking_lot", + "secrecy", + "serde_json", + "time", + "typesize-derive", + "url", +] + +[[package]] +name = "typesize-derive" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b122284365ba8497be951b9a21491f70c9688eb6fddc582931a0703f6a00ece" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "wasm-streams" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + +[[package]] +name = "webpki-roots" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..71ddf90 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "refraction" +version = "2.0.0" +edition = "2021" +repository = "https://github.com/PrismLauncher/refraction" +license = "GPL-3.0-or-later" +readme = "README.md" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[build-dependencies] +gray_matter = "0.2.6" +poise = "0.6.1" +serde = "1.0.196" +serde_json = "1.0.112" + +[dependencies] +color-eyre = "0.6.2" +dotenvy = "0.15.7" +enum_dispatch = "0.3.12" +env_logger = "0.11.1" +eyre = "0.6.11" +log = "0.4.20" +poise = "0.6.1" +octocrab = "0.37.0" +redis = { version = "0.25.2", features = ["tokio-comp", "tokio-rustls-comp"] } +regex = "1.10.3" +reqwest = { version = "0.12.2", default-features = false, features = [ + "rustls-tls", + "json", +] } +serde = "1.0.196" +serde_json = "1.0.112" +tokio = { version = "1.35.1", features = [ + "macros", + "rt-multi-thread", + "signal", +] } + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +complexity = "warn" +correctness = "deny" +pedantic = "warn" +perf = "warn" +style = "warn" +suspicious = "deny" diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 53f341b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM docker.io/library/node:21-alpine -RUN corepack enable -RUN corepack prepare pnpm@latest --activate - -WORKDIR /app - -COPY package.json pnpm-lock.yaml . -RUN pnpm install --frozen-lockfile - -COPY . . -CMD [ "pnpm", "run", "start" ] diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..5638757 --- /dev/null +++ b/build.rs @@ -0,0 +1,88 @@ +use std::io::Write; +use std::path::Path; +use std::{env, fs}; + +use gray_matter::{engine, Matter}; + +include!("src/tags.rs"); + +/// generate the `ChoiceParameter` enum and tag data we will use in the `tag` command +#[allow(dead_code)] +fn main() { + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_file = Path::new(&out_dir).join("generated.rs"); + let mut file = fs::File::create(dest_file).unwrap(); + + let tag_files: Vec = fs::read_dir(TAG_DIR) + .unwrap() + .map(|f| f.unwrap().file_name().to_string_lossy().to_string()) + .collect(); + + let mut tags: Vec = tag_files + .clone() + .into_iter() + .map(|name| { + let file_name = format!("{TAG_DIR}/{name}"); + let file_content = fs::read_to_string(&file_name).unwrap(); + + let matter = Matter::::new(); + let parsed = matter.parse(&file_content); + let content = parsed.content; + let data = parsed + .data + .unwrap() + .deserialize() + .unwrap_or_else(|e| { + // actually handling the error since this is the most likely thing to fail -getchoo + panic!( + "Failed to parse file {file_name}! Here's what it looked like:\n{content}\n\nReported Error:\n{e}\n", + ) + }); + + Tag { + content, + id: name.trim_end_matches(".md").to_string(), + frontmatter: data, + } + }) + .collect(); + + tags.sort_by(|t1, t2| t1.id.cmp(&t2.id)); + + let tag_names: Vec = tags.iter().map(|t| format!("{},", t.id)).collect(); + + let tag_matches: Vec = tags + .iter() + .map(|t| format!("Self::{} => \"{}\",", t.id, t.id)) + .collect(); + + writeln!( + file, + r#"#[allow(non_camel_case_types, clippy::upper_case_acronyms)] +#[derive(Clone, Debug, poise::ChoiceParameter)] +pub enum Choice {{ + {} +}}"#, + tag_names.join("\n") + ) + .unwrap(); + + writeln!( + file, + r#"impl Choice {{ + fn as_str(&self) -> &str {{ + match &self {{ + {} + }} + }} +}}"#, + tag_matches.join("\n") + ) + .unwrap(); + + println!( + "cargo:rustc-env=TAGS={}", + // make sure we can deserialize with env! at runtime + serde_json::to_string(&tags).unwrap() + ); +} diff --git a/flake.lock b/flake.lock index 06957f9..93b865c 100644 --- a/flake.lock +++ b/flake.lock @@ -1,85 +1,12 @@ { "nodes": { - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1673956053, - "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", - "owner": "edolstra", - "repo": "flake-compat", - "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", - "type": "github" - }, - "original": { - "owner": "edolstra", - "repo": "flake-compat", - "type": "github" - } - }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1696343447, - "narHash": "sha256-B2xAZKLkkeRFG5XcHHSXXcP7To9Xzr59KXeZiRf4vdQ=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "c9afaba3dfa4085dbd2ccb38dfade5141e33d9d4", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1685518550, - "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1660459072, - "narHash": "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=", - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "a20de23b925fd8264fd7fad6454652e142fd7f73", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, "nixpkgs": { "locked": { - "lastModified": 1696757521, - "narHash": "sha256-cfgtLNCBLFx2qOzRLI6DHfqTdfWI+UbvsKYa3b3fvaA=", + "lastModified": 1711715736, + "narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2646b294a146df2781b1ca49092450e8a32814e1", + "rev": "807c549feabce7eddbf259dbdcec9e0600a0660d", "type": "github" }, "original": { @@ -89,84 +16,9 @@ "type": "github" } }, - "nixpkgs-lib": { - "locked": { - "dir": "lib", - "lastModified": 1696019113, - "narHash": "sha256-X3+DKYWJm93DRSdC5M6K5hLqzSya9BjibtBsuARoPco=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "f5892ddac112a1e9b3612c39af1b72987ee5783a", - "type": "github" - }, - "original": { - "dir": "lib", - "owner": "NixOS", - "ref": "nixos-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, - "nixpkgs-stable": { - "locked": { - "lastModified": 1685801374, - "narHash": "sha256-otaSUoFEMM+LjBI1XL/xGB5ao6IwnZOXc47qhIgJe8U=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "c37ca420157f4abc31e26f436c1145f8951ff373", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixos-23.05", - "repo": "nixpkgs", - "type": "github" - } - }, - "pre-commit-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": "nixpkgs-stable" - }, - "locked": { - "lastModified": 1696846637, - "narHash": "sha256-0hv4kbXxci2+pxhuXlVgftj/Jq79VSmtAyvfabCCtYk=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "42e1b6095ef80a51f79595d9951eb38e91c4e6ca", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "type": "github" - } - }, "root": { "inputs": { - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": "pre-commit-hooks" - } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" + "nixpkgs": "nixpkgs" } } }, diff --git a/flake.nix b/flake.nix index 9b8d655..e575b27 100644 --- a/flake.nix +++ b/flake.nix @@ -1,52 +1,31 @@ { description = "Discord bot for Prism Launcher"; - inputs = { - nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; - flake-parts.url = "github:hercules-ci/flake-parts"; - pre-commit-hooks = { - url = "github:cachix/pre-commit-hooks.nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - }; + inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; outputs = { - flake-parts, - pre-commit-hooks, + self, + nixpkgs, ... - } @ inputs: - flake-parts.lib.mkFlake {inherit inputs;} { - imports = [ - pre-commit-hooks.flakeModule - ]; + }: let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; - perSystem = { - config, - lib, - pkgs, - ... - }: { - pre-commit.settings.hooks = { - alejandra.enable = true; - prettier = { - enable = true; - excludes = ["flake.lock" "pnpm-lock.yaml"]; - }; - }; - devShells.default = pkgs.mkShell { - shellHook = '' - ${config.pre-commit.installationScript} - ''; - packages = with pkgs; [nodePackages.pnpm redis]; - }; - formatter = pkgs.alejandra; - }; + forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); + in { + nixosModules.default = import ./nix/module.nix self; - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; + packages = forAllSystems (pkgs: rec { + refraction = pkgs.callPackage ./nix/derivation.nix {inherit self;}; + default = refraction; + }); + + overlays.default = _: prev: { + refraction = prev.callPackage ./nix/derivation.nix {inherit self;}; }; + }; } diff --git a/nix/derivation.nix b/nix/derivation.nix new file mode 100644 index 0000000..9d95a29 --- /dev/null +++ b/nix/derivation.nix @@ -0,0 +1,63 @@ +{ + lib, + stdenv, + rustPlatform, + darwin, + self, + lto ? true, + optimizeSize ? false, +}: +rustPlatform.buildRustPackage { + pname = "refraction"; + version = + (lib.importTOML ../Cargo.toml).package.version + + "-${self.shortRev or self.dirtyShortRev or "unknown-dirty"}"; + + __structuredAttrs = true; + + src = lib.fileset.toSource { + root = ../.; + fileset = lib.fileset.unions [ + ../src + ../build.rs + ../Cargo.lock + ../Cargo.toml + ../tags + ]; + }; + + cargoLock = { + lockFile = ../Cargo.lock; + }; + + buildInputs = lib.optionals stdenv.hostPlatform.isDarwin (with darwin.apple_sdk.frameworks; [ + CoreFoundation + Security + SystemConfiguration + ]); + + env = let + toRustFlags = lib.mapAttrs' ( + name: + lib.nameValuePair + "CARGO_PROFILE_RELEASE_${lib.toUpper (builtins.replaceStrings ["-"] ["_"] name)}" + ); + in + lib.optionalAttrs lto (toRustFlags { + lto = "thin"; + }) + // lib.optionalAttrs optimizeSize (toRustFlags { + codegen-units = "1"; + opt-level = "s"; + panic = "abort"; + strip = "symbols"; + }); + + meta = with lib; { + mainProgram = "refraction"; + description = "Discord bot for Prism Launcher"; + homepage = "https://github.com/PrismLauncher/refraction"; + license = licenses.gpl3Plus; + maintainers = with maintainers; [getchoo Scrumplex]; + }; +} diff --git a/nix/dev/args.nix b/nix/dev/args.nix new file mode 100644 index 0000000..42b60e2 --- /dev/null +++ b/nix/dev/args.nix @@ -0,0 +1,11 @@ +{inputs, ...}: { + perSystem = { + lib, + system, + ... + }: { + _module.args = { + refraction' = lib.mapAttrs (lib.const (v: v.${system} or v)) (inputs.get-flake ../../.); + }; + }; +} diff --git a/nix/dev/docker.nix b/nix/dev/docker.nix new file mode 100644 index 0000000..f778070 --- /dev/null +++ b/nix/dev/docker.nix @@ -0,0 +1,25 @@ +{withSystem, ...}: { + perSystem = { + lib, + pkgs, + self', + ... + }: let + containerFor = arch: + pkgs.dockerTools.buildLayeredImage { + name = "refraction"; + tag = "latest-${arch}"; + contents = [pkgs.dockerTools.caCertificates]; + config.Cmd = [ + (lib.getExe self'.packages."refraction-static-${arch}") + ]; + + architecture = withSystem "${arch}-linux" ({pkgs, ...}: pkgs.pkgsStatic.go.GOARCH); + }; + in { + packages = { + container-x86_64 = containerFor "x86_64"; + container-aarch64 = containerFor "aarch64"; + }; + }; +} diff --git a/nix/dev/flake.lock b/nix/dev/flake.lock new file mode 100644 index 0000000..8522cb4 --- /dev/null +++ b/nix/dev/flake.lock @@ -0,0 +1,212 @@ +{ + "nodes": { + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709336216, + "narHash": "sha256-Dt/wOWeW6Sqm11Yh+2+t0dfEWxoMxGBvv3JpIocFl9E=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f7b3c975cf067e56e7cda6cb098ebe3fb4d74ca2", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "get-flake": { + "locked": { + "lastModified": 1694475786, + "narHash": "sha256-s5wDmPooMUNIAAsxxCMMh9g68AueGg63DYk2hVZJbc8=", + "owner": "ursi", + "repo": "get-flake", + "rev": "ac54750e3b95dab6ec0726d77f440efe6045bec1", + "type": "github" + }, + "original": { + "owner": "ursi", + "repo": "get-flake", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "pre-commit-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711715736, + "narHash": "sha256-9slQ609YqT9bT/MNX9+5k5jltL9zgpn36DpFB7TkttM=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "807c549feabce7eddbf259dbdcec9e0600a0660d", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-compat": [], + "flake-utils": "flake-utils", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ], + "nixpkgs-stable": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711850184, + "narHash": "sha256-rs5zMkTO+AlVBzgOaskAtY4zix7q3l8PpawfznHotcQ=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "9fc61b5eb0e50fc42f1d358f5240722907b79726", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "procfile-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711158989, + "narHash": "sha256-exgncIe/lQIswv2L1M0y+RrHAg5dofLFCOxGu4/yJww=", + "owner": "getchoo", + "repo": "procfile-nix", + "rev": "6388308f9e9c8a8fbfdff54b30adf486fa292cf9", + "type": "github" + }, + "original": { + "owner": "getchoo", + "repo": "procfile-nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-parts": "flake-parts", + "get-flake": "get-flake", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "procfile-nix": "procfile-nix", + "rust-overlay": "rust-overlay", + "treefmt-nix": "treefmt-nix" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "pre-commit-hooks", + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711851236, + "narHash": "sha256-EJ03x3N9ihhonAttkaCrqxb0djDq3URCuDpmVPbNZhA=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f258266af947599e8069df1c2e933189270f143a", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711803027, + "narHash": "sha256-Qic3OvsVLpetchzaIe2hJqgliWXACq2Oee6mBXa/IZQ=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "1810d51a015c1730f2fe05a255258649799df416", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/nix/dev/flake.nix b/nix/dev/flake.nix new file mode 100644 index 0000000..0f99cb1 --- /dev/null +++ b/nix/dev/flake.nix @@ -0,0 +1,65 @@ +{ + description = "Discord bot for Prism Launcher"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + + get-flake.url = "github:ursi/get-flake"; + + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs = { + nixpkgs.follows = "nixpkgs"; + nixpkgs-stable.follows = "nixpkgs"; + flake-compat.follows = ""; + }; + }; + + procfile-nix = { + url = "github:getchoo/procfile-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs = { + nixpkgs.follows = "nixpkgs"; + flake-utils.follows = "pre-commit-hooks/flake-utils"; + }; + }; + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = {flake-parts, ...} @ inputs: + flake-parts.lib.mkFlake {inherit inputs;} { + debug = true; + imports = [ + ./args.nix + ./docker.nix + ./pre-commit.nix + ./procfiles.nix + ./shell.nix + ./static.nix + ./treefmt.nix + + inputs.pre-commit-hooks.flakeModule + inputs.procfile-nix.flakeModule + inputs.treefmt-nix.flakeModule + ]; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + }; +} diff --git a/nix/dev/pre-commit.nix b/nix/dev/pre-commit.nix new file mode 100644 index 0000000..39d6e9e --- /dev/null +++ b/nix/dev/pre-commit.nix @@ -0,0 +1,20 @@ +{ + perSystem = { + config, + lib, + ... + }: { + pre-commit.settings = { + rootSrc = lib.mkForce ../../.; + hooks = { + actionlint.enable = true; + nil.enable = true; + statix.enable = true; + treefmt = { + enable = true; + package = config.treefmt.build.wrapper; + }; + }; + }; + }; +} diff --git a/nix/dev/procfiles.nix b/nix/dev/procfiles.nix new file mode 100644 index 0000000..c8c8c2e --- /dev/null +++ b/nix/dev/procfiles.nix @@ -0,0 +1,11 @@ +{ + perSystem = { + lib, + pkgs, + ... + }: { + procfiles.daemons.processes = { + redis = lib.getExe' pkgs.redis "redis-server"; + }; + }; +} diff --git a/nix/dev/shell.nix b/nix/dev/shell.nix new file mode 100644 index 0000000..e0adb94 --- /dev/null +++ b/nix/dev/shell.nix @@ -0,0 +1,36 @@ +{ + perSystem = { + pkgs, + config, + self', + refraction', + ... + }: { + devShells.default = pkgs.mkShell { + shellHook = '' + ${config.pre-commit.installationScript} + ''; + + packages = with pkgs; [ + # general + actionlint + nodePackages.prettier + config.procfiles.daemons.package + + # rust + clippy + rustfmt + rust-analyzer + + # nix + self'.formatter + deadnix + nil + statix + ]; + + inputsFrom = [refraction'.packages.refraction]; + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; + }; +} diff --git a/nix/dev/static.nix b/nix/dev/static.nix new file mode 100644 index 0000000..3a1c470 --- /dev/null +++ b/nix/dev/static.nix @@ -0,0 +1,41 @@ +{ + perSystem = { + lib, + pkgs, + inputs', + refraction', + ... + }: let + targets = with pkgs.pkgsCross; { + x86_64 = musl64.pkgsStatic; + aarch64 = aarch64-multiplatform.pkgsStatic; + }; + + toolchain = inputs'.rust-overlay.packages.rust.minimal.override { + extensions = ["rust-std"]; + targets = map (pkgs: pkgs.stdenv.hostPlatform.config) (lib.attrValues targets); + }; + + rustPlatforms = + lib.mapAttrs ( + lib.const (pkgs: + pkgs.makeRustPlatform ( + lib.genAttrs ["cargo" "rustc"] (lib.const toolchain) + )) + ) + targets; + + buildWith = rustPlatform: + refraction'.packages.refraction.override { + inherit rustPlatform; + optimizeSize = true; + }; + in { + packages = + lib.mapAttrs' ( + target: rustPlatform: + lib.nameValuePair "refraction-static-${target}" (buildWith rustPlatform) + ) + rustPlatforms; + }; +} diff --git a/nix/dev/treefmt.nix b/nix/dev/treefmt.nix new file mode 100644 index 0000000..13a1675 --- /dev/null +++ b/nix/dev/treefmt.nix @@ -0,0 +1,22 @@ +{ + perSystem = { + treefmt = { + projectRootFile = ".git/config"; + + programs = { + alejandra.enable = true; + deadnix.enable = true; + prettier.enable = true; + rustfmt.enable = true; + }; + + settings.global = { + excludes = [ + "./target" + "./flake.lock" + "./Cargo.lock" + ]; + }; + }; + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..5572e4b --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,149 @@ +self: { + config, + lib, + pkgs, + ... +}: let + cfg = config.services.refraction; + defaultUser = "refraction"; + + inherit + (lib) + getExe + literalExpression + mdDoc + mkEnableOption + mkIf + mkOption + mkPackageOption + optionals + types + ; +in { + options.services.refraction = { + enable = mkEnableOption "refraction"; + package = mkPackageOption self.packages.${pkgs.stdenv.hostPlatform.system} "refraction" {}; + + user = mkOption { + description = mdDoc '' + User under which the service should run. If this is the default value, + the user will be created, with the specified group as the primary + group. + ''; + type = types.str; + default = defaultUser; + example = literalExpression '' + "bob" + ''; + }; + + group = mkOption { + description = mdDoc '' + Group under which the service should run. If this is the default value, + the group will be created. + ''; + type = types.str; + default = defaultUser; + example = literalExpression '' + "discordbots" + ''; + }; + + redisUrl = mkOption { + description = mdDoc '' + Connection to a redis server. If this needs to include credentials + that shouldn't be world-readable in the Nix store, set environmentFile + and override the `REDIS_URL` entry. + Pass the string `local` to setup a local Redis database. + ''; + type = types.str; + default = "local"; + example = literalExpression '' + "redis://localhost/" + ''; + }; + + environmentFile = mkOption { + description = mdDoc '' + Environment file as defined in {manpage}`systemd.exec(5)` + ''; + type = types.nullOr types.path; + default = null; + example = literalExpression '' + "/run/agenix.d/1/refraction" + ''; + }; + }; + + config = mkIf cfg.enable { + services.redis.servers.refraction = mkIf (cfg.redisUrl == "local") { + enable = true; + inherit (cfg) user; + port = 0; # disable tcp listener + }; + + systemd.services."refraction" = { + enable = true; + wantedBy = ["multi-user.target"]; + after = + ["network.target"] + ++ optionals (cfg.redisUrl == "local") ["redis-refraction.service"]; + + script = '' + ${getExe cfg.package} + ''; + + environment = { + BOT_REDIS_URL = + if cfg.redisUrl == "local" + then "unix:${config.services.redis.servers.refraction.unixSocket}" + else cfg.redisUrl; + }; + + serviceConfig = { + Type = "simple"; + Restart = "on-failure"; + + EnvironmentFile = mkIf (cfg.environmentFile != null) cfg.environmentFile; + + User = cfg.user; + Group = cfg.group; + + # hardening + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@resources" + "~@privileged" + ]; + }; + }; + + users = { + users = mkIf (cfg.user == defaultUser) { + ${defaultUser} = { + isSystemUser = true; + inherit (cfg) group; + }; + }; + + groups = mkIf (cfg.group == defaultUser) { + ${defaultUser} = {}; + }; + }; + }; +} diff --git a/package.json b/package.json deleted file mode 100644 index 5ec4f53..0000000 --- a/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "refraction", - "version": "1.0.0", - "license": "GPL-3.0", - "scripts": { - "dev": "NODE_ENV=development tsx watch src/index.ts", - "start": "tsx src/index.ts", - "reupload": "tsx src/_reupload.ts", - "lint": "tsc && eslint ." - }, - "dependencies": { - "@discordjs/rest": "2.1.0", - "discord.js": "14.14.1", - "just-random": "3.2.0", - "kleur": "4.1.5", - "redis": "4.6.10", - "tsx": "4.1.1" - }, - "devDependencies": { - "@types/node": "20.9.0", - "@typescript-eslint/eslint-plugin": "6.10.0", - "@typescript-eslint/parser": "6.10.0", - "dotenv": "16.3.1", - "eslint": "8.53.0", - "gray-matter": "4.0.3", - "prettier": "3.0.3", - "typescript": "5.2.2" - }, - "packageManager": "pnpm@8.10.3" -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml deleted file mode 100644 index dd97a44..0000000 --- a/pnpm-lock.yaml +++ /dev/null @@ -1,1562 +0,0 @@ -lockfileVersion: '6.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -dependencies: - '@discordjs/rest': - specifier: 2.1.0 - version: 2.1.0 - discord.js: - specifier: 14.14.1 - version: 14.14.1 - just-random: - specifier: 3.2.0 - version: 3.2.0 - kleur: - specifier: 4.1.5 - version: 4.1.5 - redis: - specifier: 4.6.10 - version: 4.6.10 - tsx: - specifier: 4.1.1 - version: 4.1.1 - -devDependencies: - '@types/node': - specifier: 20.9.0 - version: 20.9.0 - '@typescript-eslint/eslint-plugin': - specifier: 6.10.0 - version: 6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/parser': - specifier: 6.10.0 - version: 6.10.0(eslint@8.53.0)(typescript@5.2.2) - dotenv: - specifier: 16.3.1 - version: 16.3.1 - eslint: - specifier: 8.53.0 - version: 8.53.0 - gray-matter: - specifier: 4.0.3 - version: 4.0.3 - prettier: - specifier: 3.0.3 - version: 3.0.3 - typescript: - specifier: 5.2.2 - version: 5.2.2 - -packages: - - /@aashutoshrathi/word-wrap@1.2.6: - resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} - engines: {node: '>=0.10.0'} - dev: true - - /@discordjs/builders@1.7.0: - resolution: {integrity: sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/formatters': 0.3.3 - '@discordjs/util': 1.0.2 - '@sapphire/shapeshift': 3.9.3 - discord-api-types: 0.37.61 - fast-deep-equal: 3.1.3 - ts-mixer: 6.0.3 - tslib: 2.6.2 - dev: false - - /@discordjs/collection@1.5.3: - resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} - engines: {node: '>=16.11.0'} - dev: false - - /@discordjs/collection@2.0.0: - resolution: {integrity: sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==} - engines: {node: '>=18'} - dev: false - - /@discordjs/formatters@0.3.3: - resolution: {integrity: sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==} - engines: {node: '>=16.11.0'} - dependencies: - discord-api-types: 0.37.61 - dev: false - - /@discordjs/rest@2.1.0: - resolution: {integrity: sha512-5gFWFkZX2JCFSRzs8ltx8bWmyVi0wPMk6pBa9KGIQSDPMmrP+uOrZ9j9HOwvmVWGe+LmZ5Bov0jMnQd6/jVReg==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/collection': 2.0.0 - '@discordjs/util': 1.0.2 - '@sapphire/async-queue': 1.5.0 - '@sapphire/snowflake': 3.5.1 - '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.61 - magic-bytes.js: 1.5.0 - tslib: 2.6.2 - undici: 5.27.2 - dev: false - - /@discordjs/util@1.0.2: - resolution: {integrity: sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==} - engines: {node: '>=16.11.0'} - dev: false - - /@discordjs/ws@1.0.2: - resolution: {integrity: sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/collection': 2.0.0 - '@discordjs/rest': 2.1.0 - '@discordjs/util': 1.0.2 - '@sapphire/async-queue': 1.5.0 - '@types/ws': 8.5.9 - '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.61 - tslib: 2.6.2 - ws: 8.14.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /@esbuild/android-arm64@0.18.20: - resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@esbuild/android-arm@0.18.20: - resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@esbuild/android-x64@0.18.20: - resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: false - optional: true - - /@esbuild/darwin-arm64@0.18.20: - resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@esbuild/darwin-x64@0.18.20: - resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /@esbuild/freebsd-arm64@0.18.20: - resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - - /@esbuild/freebsd-x64@0.18.20: - resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-arm64@0.18.20: - resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-arm@0.18.20: - resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-ia32@0.18.20: - resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-loong64@0.18.20: - resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-mips64el@0.18.20: - resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-ppc64@0.18.20: - resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-riscv64@0.18.20: - resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-s390x@0.18.20: - resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/linux-x64@0.18.20: - resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false - optional: true - - /@esbuild/netbsd-x64@0.18.20: - resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: false - optional: true - - /@esbuild/openbsd-x64@0.18.20: - resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: false - optional: true - - /@esbuild/sunos-x64@0.18.20: - resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: false - optional: true - - /@esbuild/win32-arm64@0.18.20: - resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@esbuild/win32-ia32@0.18.20: - resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@esbuild/win32-x64@0.18.20: - resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false - optional: true - - /@eslint-community/eslint-utils@4.4.0(eslint@8.53.0): - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - dependencies: - eslint: 8.53.0 - eslint-visitor-keys: 3.4.3 - dev: true - - /@eslint-community/regexpp@4.6.2: - resolution: {integrity: sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - dev: true - - /@eslint/eslintrc@2.1.3: - resolution: {integrity: sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - ajv: 6.12.6 - debug: 4.3.4 - espree: 9.6.1 - globals: 13.20.0 - ignore: 5.2.4 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color - dev: true - - /@eslint/js@8.53.0: - resolution: {integrity: sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /@fastify/busboy@2.1.0: - resolution: {integrity: sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==} - engines: {node: '>=14'} - dev: false - - /@humanwhocodes/config-array@0.11.13: - resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} - engines: {node: '>=10.10.0'} - dependencies: - '@humanwhocodes/object-schema': 2.0.1 - debug: 4.3.4 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@humanwhocodes/module-importer@1.0.1: - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} - engines: {node: '>=12.22'} - dev: true - - /@humanwhocodes/object-schema@2.0.1: - resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} - dev: true - - /@nodelib/fs.scandir@2.1.5: - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - dev: true - - /@nodelib/fs.stat@2.0.5: - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - dev: true - - /@nodelib/fs.walk@1.2.8: - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.15.0 - dev: true - - /@redis/bloom@1.2.0(@redis/client@1.5.11): - resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==} - peerDependencies: - '@redis/client': ^1.0.0 - dependencies: - '@redis/client': 1.5.11 - dev: false - - /@redis/client@1.5.11: - resolution: {integrity: sha512-cV7yHcOAtNQ5x/yQl7Yw1xf53kO0FNDTdDU6bFIMbW6ljB7U7ns0YRM+QIkpoqTAt6zK5k9Fq0QWlUbLcq9AvA==} - engines: {node: '>=14'} - dependencies: - cluster-key-slot: 1.1.2 - generic-pool: 3.9.0 - yallist: 4.0.0 - dev: false - - /@redis/graph@1.1.0(@redis/client@1.5.11): - resolution: {integrity: sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==} - peerDependencies: - '@redis/client': ^1.0.0 - dependencies: - '@redis/client': 1.5.11 - dev: false - - /@redis/json@1.0.6(@redis/client@1.5.11): - resolution: {integrity: sha512-rcZO3bfQbm2zPRpqo82XbW8zg4G/w4W3tI7X8Mqleq9goQjAGLL7q/1n1ZX4dXEAmORVZ4s1+uKLaUOg7LrUhw==} - peerDependencies: - '@redis/client': ^1.0.0 - dependencies: - '@redis/client': 1.5.11 - dev: false - - /@redis/search@1.1.5(@redis/client@1.5.11): - resolution: {integrity: sha512-hPP8w7GfGsbtYEJdn4n7nXa6xt6hVZnnDktKW4ArMaFQ/m/aR7eFvsLQmG/mn1Upq99btPJk+F27IQ2dYpCoUg==} - peerDependencies: - '@redis/client': ^1.0.0 - dependencies: - '@redis/client': 1.5.11 - dev: false - - /@redis/time-series@1.0.5(@redis/client@1.5.11): - resolution: {integrity: sha512-IFjIgTusQym2B5IZJG3XKr5llka7ey84fw/NOYqESP5WUfQs9zz1ww/9+qoz4ka/S6KcGBodzlCeZ5UImKbscg==} - peerDependencies: - '@redis/client': ^1.0.0 - dependencies: - '@redis/client': 1.5.11 - dev: false - - /@sapphire/async-queue@1.5.0: - resolution: {integrity: sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /@sapphire/shapeshift@3.9.3: - resolution: {integrity: sha512-WzKJSwDYloSkHoBbE8rkRW8UNKJiSRJ/P8NqJ5iVq7U2Yr/kriIBx2hW+wj2Z5e5EnXL1hgYomgaFsdK6b+zqQ==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dependencies: - fast-deep-equal: 3.1.3 - lodash: 4.17.21 - dev: false - - /@sapphire/snowflake@3.5.1: - resolution: {integrity: sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /@types/json-schema@7.0.12: - resolution: {integrity: sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==} - dev: true - - /@types/node@20.9.0: - resolution: {integrity: sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==} - dependencies: - undici-types: 5.26.5 - - /@types/semver@7.5.0: - resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} - dev: true - - /@types/ws@8.5.9: - resolution: {integrity: sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==} - dependencies: - '@types/node': 20.9.0 - dev: false - - /@typescript-eslint/eslint-plugin@6.10.0(@typescript-eslint/parser@6.10.0)(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-uoLj4g2OTL8rfUQVx2AFO1hp/zja1wABJq77P6IclQs6I/m9GLrm7jCdgzZkvWdDCQf1uEvoa8s8CupsgWQgVg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@eslint-community/regexpp': 4.6.2 - '@typescript-eslint/parser': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/scope-manager': 6.10.0 - '@typescript-eslint/type-utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.4 - eslint: 8.53.0 - graphemer: 1.4.0 - ignore: 5.2.4 - natural-compare: 1.4.0 - semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/parser@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-+sZwIj+s+io9ozSxIWbNB5873OSdfeBEH/FR0re14WLI6BaKuSOnnwCJ2foUiu8uXf4dRp1UqHP0vrZ1zXGrog==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/scope-manager': 6.10.0 - '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) - '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.4 - eslint: 8.53.0 - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/scope-manager@6.10.0: - resolution: {integrity: sha512-TN/plV7dzqqC2iPNf1KrxozDgZs53Gfgg5ZHyw8erd6jd5Ta/JIEcdCheXFt9b1NYb93a1wmIIVW/2gLkombDg==} - engines: {node: ^16.0.0 || >=18.0.0} - dependencies: - '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/visitor-keys': 6.10.0 - dev: true - - /@typescript-eslint/type-utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-wYpPs3hgTFblMYwbYWPT3eZtaDOjbLyIYuqpwuLBBqhLiuvJ+9sEp2gNRJEtR5N/c9G1uTtQQL5AhV0fEPJYcg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) - '@typescript-eslint/utils': 6.10.0(eslint@8.53.0)(typescript@5.2.2) - debug: 4.3.4 - eslint: 8.53.0 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/types@6.10.0: - resolution: {integrity: sha512-36Fq1PWh9dusgo3vH7qmQAj5/AZqARky1Wi6WpINxB6SkQdY5vQoT2/7rW7uBIsPDcvvGCLi4r10p0OJ7ITAeg==} - engines: {node: ^16.0.0 || >=18.0.0} - dev: true - - /@typescript-eslint/typescript-estree@6.10.0(typescript@5.2.2): - resolution: {integrity: sha512-ek0Eyuy6P15LJVeghbWhSrBCj/vJpPXXR+EpaRZqou7achUWL8IdYnMSC5WHAeTWswYQuP2hAZgij/bC9fanBg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - dependencies: - '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/visitor-keys': 6.10.0 - debug: 4.3.4 - globby: 11.1.0 - is-glob: 4.0.3 - semver: 7.5.4 - ts-api-utils: 1.0.1(typescript@5.2.2) - typescript: 5.2.2 - transitivePeerDependencies: - - supports-color - dev: true - - /@typescript-eslint/utils@6.10.0(eslint@8.53.0)(typescript@5.2.2): - resolution: {integrity: sha512-v+pJ1/RcVyRc0o4wAGux9x42RHmAjIGzPRo538Z8M1tVx6HOnoQBCX/NoadHQlZeC+QO2yr4nNSFWOoraZCAyg==} - engines: {node: ^16.0.0 || >=18.0.0} - peerDependencies: - eslint: ^7.0.0 || ^8.0.0 - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@types/json-schema': 7.0.12 - '@types/semver': 7.5.0 - '@typescript-eslint/scope-manager': 6.10.0 - '@typescript-eslint/types': 6.10.0 - '@typescript-eslint/typescript-estree': 6.10.0(typescript@5.2.2) - eslint: 8.53.0 - semver: 7.5.4 - transitivePeerDependencies: - - supports-color - - typescript - dev: true - - /@typescript-eslint/visitor-keys@6.10.0: - resolution: {integrity: sha512-xMGluxQIEtOM7bqFCo+rCMh5fqI+ZxV5RUUOa29iVPz1OgCZrtc7rFnz5cLUazlkPKYqX+75iuDq7m0HQ48nCg==} - engines: {node: ^16.0.0 || >=18.0.0} - dependencies: - '@typescript-eslint/types': 6.10.0 - eslint-visitor-keys: 3.4.3 - dev: true - - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: true - - /@vladfrangu/async_event_emitter@2.2.2: - resolution: {integrity: sha512-HIzRG7sy88UZjBJamssEczH5q7t5+axva19UbZLO6u0ySbYPrwzWiXBcC0WuHyhKKoeCyneH+FvYzKQq/zTtkQ==} - engines: {node: '>=v14.0.0', npm: '>=7.0.0'} - dev: false - - /acorn-jsx@5.3.2(acorn@8.9.0): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - dependencies: - acorn: 8.9.0 - dev: true - - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: true - - /ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - dev: true - - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - - /ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - dependencies: - color-convert: 2.0.1 - dev: true - - /argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - dependencies: - sprintf-js: 1.0.3 - dev: true - - /argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - dev: true - - /array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - dev: true - - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true - - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - dependencies: - balanced-match: 1.0.2 - concat-map: 0.0.1 - dev: true - - /braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - dependencies: - fill-range: 7.0.1 - dev: true - - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false - - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - dev: true - - /cluster-key-slot@1.1.2: - resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} - engines: {node: '>=0.10.0'} - dev: false - - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - dependencies: - color-name: 1.1.4 - dev: true - - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: true - - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true - - /cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} - engines: {node: '>= 8'} - dependencies: - path-key: 3.1.1 - shebang-command: 2.0.0 - which: 2.0.2 - dev: true - - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - dev: true - - /deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - dev: true - - /dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} - engines: {node: '>=8'} - dependencies: - path-type: 4.0.0 - dev: true - - /discord-api-types@0.37.61: - resolution: {integrity: sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==} - dev: false - - /discord.js@14.14.1: - resolution: {integrity: sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==} - engines: {node: '>=16.11.0'} - dependencies: - '@discordjs/builders': 1.7.0 - '@discordjs/collection': 1.5.3 - '@discordjs/formatters': 0.3.3 - '@discordjs/rest': 2.1.0 - '@discordjs/util': 1.0.2 - '@discordjs/ws': 1.0.2 - '@sapphire/snowflake': 3.5.1 - '@types/ws': 8.5.9 - discord-api-types: 0.37.61 - fast-deep-equal: 3.1.3 - lodash.snakecase: 4.1.1 - tslib: 2.6.2 - undici: 5.27.2 - ws: 8.14.2 - transitivePeerDependencies: - - bufferutil - - utf-8-validate - dev: false - - /doctrine@3.0.0: - resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} - engines: {node: '>=6.0.0'} - dependencies: - esutils: 2.0.3 - dev: true - - /dotenv@16.3.1: - resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} - engines: {node: '>=12'} - dev: true - - /esbuild@0.18.20: - resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/android-arm': 0.18.20 - '@esbuild/android-arm64': 0.18.20 - '@esbuild/android-x64': 0.18.20 - '@esbuild/darwin-arm64': 0.18.20 - '@esbuild/darwin-x64': 0.18.20 - '@esbuild/freebsd-arm64': 0.18.20 - '@esbuild/freebsd-x64': 0.18.20 - '@esbuild/linux-arm': 0.18.20 - '@esbuild/linux-arm64': 0.18.20 - '@esbuild/linux-ia32': 0.18.20 - '@esbuild/linux-loong64': 0.18.20 - '@esbuild/linux-mips64el': 0.18.20 - '@esbuild/linux-ppc64': 0.18.20 - '@esbuild/linux-riscv64': 0.18.20 - '@esbuild/linux-s390x': 0.18.20 - '@esbuild/linux-x64': 0.18.20 - '@esbuild/netbsd-x64': 0.18.20 - '@esbuild/openbsd-x64': 0.18.20 - '@esbuild/sunos-x64': 0.18.20 - '@esbuild/win32-arm64': 0.18.20 - '@esbuild/win32-ia32': 0.18.20 - '@esbuild/win32-x64': 0.18.20 - dev: false - - /escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - dev: true - - /eslint-scope@7.2.2: - resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - esrecurse: 4.3.0 - estraverse: 5.3.0 - dev: true - - /eslint-visitor-keys@3.4.3: - resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dev: true - - /eslint@8.53.0: - resolution: {integrity: sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - hasBin: true - dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@8.53.0) - '@eslint-community/regexpp': 4.6.2 - '@eslint/eslintrc': 2.1.3 - '@eslint/js': 8.53.0 - '@humanwhocodes/config-array': 0.11.13 - '@humanwhocodes/module-importer': 1.0.1 - '@nodelib/fs.walk': 1.2.8 - '@ungap/structured-clone': 1.2.0 - ajv: 6.12.6 - chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.4 - doctrine: 3.0.0 - escape-string-regexp: 4.0.0 - eslint-scope: 7.2.2 - eslint-visitor-keys: 3.4.3 - espree: 9.6.1 - esquery: 1.5.0 - esutils: 2.0.3 - fast-deep-equal: 3.1.3 - file-entry-cache: 6.0.1 - find-up: 5.0.0 - glob-parent: 6.0.2 - globals: 13.20.0 - graphemer: 1.4.0 - ignore: 5.2.4 - imurmurhash: 0.1.4 - is-glob: 4.0.3 - is-path-inside: 3.0.3 - js-yaml: 4.1.0 - json-stable-stringify-without-jsonify: 1.0.1 - levn: 0.4.1 - lodash.merge: 4.6.2 - minimatch: 3.1.2 - natural-compare: 1.4.0 - optionator: 0.9.3 - strip-ansi: 6.0.1 - text-table: 0.2.0 - transitivePeerDependencies: - - supports-color - dev: true - - /espree@9.6.1: - resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - dependencies: - acorn: 8.9.0 - acorn-jsx: 5.3.2(acorn@8.9.0) - eslint-visitor-keys: 3.4.3 - dev: true - - /esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - dev: true - - /esquery@1.5.0: - resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} - engines: {node: '>=0.10'} - dependencies: - estraverse: 5.3.0 - dev: true - - /esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} - engines: {node: '>=4.0'} - dependencies: - estraverse: 5.3.0 - dev: true - - /estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - dev: true - - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - - /extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - dependencies: - is-extendable: 0.1.1 - dev: true - - /fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} - engines: {node: '>=8.6.0'} - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.5 - dev: true - - /fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} - dev: true - - /fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - dev: true - - /fastq@1.15.0: - resolution: {integrity: sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==} - dependencies: - reusify: 1.0.4 - dev: true - - /file-entry-cache@6.0.1: - resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flat-cache: 3.0.4 - dev: true - - /fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - dependencies: - to-regex-range: 5.0.1 - dev: true - - /find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - dependencies: - locate-path: 6.0.0 - path-exists: 4.0.0 - dev: true - - /flat-cache@3.0.4: - resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} - engines: {node: ^10.12.0 || >=12.0.0} - dependencies: - flatted: 3.2.7 - rimraf: 3.0.2 - dev: true - - /flatted@3.2.7: - resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} - dev: true - - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true - - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: false - optional: true - - /generic-pool@3.9.0: - resolution: {integrity: sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==} - engines: {node: '>= 4'} - dev: false - - /get-tsconfig@4.7.2: - resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} - dependencies: - resolve-pkg-maps: 1.0.0 - dev: false - - /glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} - engines: {node: '>=10.13.0'} - dependencies: - is-glob: 4.0.3 - dev: true - - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - dependencies: - fs.realpath: 1.0.0 - inflight: 1.0.6 - inherits: 2.0.4 - minimatch: 3.1.2 - once: 1.4.0 - path-is-absolute: 1.0.1 - dev: true - - /globals@13.20.0: - resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} - engines: {node: '>=8'} - dependencies: - type-fest: 0.20.2 - dev: true - - /globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.2.12 - ignore: 5.2.4 - merge2: 1.4.1 - slash: 3.0.0 - dev: true - - /graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - dev: true - - /gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - dependencies: - js-yaml: 3.14.1 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - dev: true - - /has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - dev: true - - /ignore@5.2.4: - resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==} - engines: {node: '>= 4'} - dev: true - - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} - dependencies: - parent-module: 1.0.1 - resolve-from: 4.0.0 - dev: true - - /imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} - engines: {node: '>=0.8.19'} - dev: true - - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - dependencies: - once: 1.4.0 - wrappy: 1.0.2 - dev: true - - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - dev: true - - /is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - dev: true - - /is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - dev: true - - /is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - dependencies: - is-extglob: 2.1.1 - dev: true - - /is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - dev: true - - /is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - dev: true - - /isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - dev: true - - /js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} - hasBin: true - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - dev: true - - /js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - dependencies: - argparse: 2.0.1 - dev: true - - /json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} - dev: true - - /json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - dev: true - - /just-random@3.2.0: - resolution: {integrity: sha512-RMf8vbtCfLIbAEHvIPu2FwMkpB/JudGyk/VPfqPItcRgt7k8QnV+Aa7s7kRFPo+bavQkUi8Yg1x/ooW6Ttyb9A==} - dev: false - - /kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - dev: true - - /kleur@4.1.5: - resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} - engines: {node: '>=6'} - dev: false - - /levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - dependencies: - p-locate: 5.0.0 - dev: true - - /lodash.merge@4.6.2: - resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - dev: true - - /lodash.snakecase@4.1.1: - resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} - dev: false - - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false - - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /magic-bytes.js@1.5.0: - resolution: {integrity: sha512-wJkXvutRbNWcc37tt5j1HyOK1nosspdh3dj6LUYYAvF6JYNqs53IfRvK9oEpcwiDA1NdoIi64yAMfdivPeVAyw==} - dev: false - - /merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - dev: true - - /micromatch@4.0.5: - resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} - engines: {node: '>=8.6'} - dependencies: - braces: 3.0.2 - picomatch: 2.3.1 - dev: true - - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - dependencies: - brace-expansion: 1.1.11 - dev: true - - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true - - /natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - dev: true - - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - dependencies: - wrappy: 1.0.2 - dev: true - - /optionator@0.9.3: - resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} - engines: {node: '>= 0.8.0'} - dependencies: - '@aashutoshrathi/word-wrap': 1.2.6 - deep-is: 0.1.4 - fast-levenshtein: 2.0.6 - levn: 0.4.1 - prelude-ls: 1.2.1 - type-check: 0.4.0 - dev: true - - /p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - dependencies: - yocto-queue: 0.1.0 - dev: true - - /p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - dependencies: - p-limit: 3.1.0 - dev: true - - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} - dependencies: - callsites: 3.1.0 - dev: true - - /path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - dev: true - - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true - - /path-key@3.1.1: - resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} - engines: {node: '>=8'} - dev: true - - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true - - /picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - dev: true - - /prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} - engines: {node: '>= 0.8.0'} - dev: true - - /prettier@3.0.3: - resolution: {integrity: sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==} - engines: {node: '>=14'} - hasBin: true - dev: true - - /punycode@2.3.0: - resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} - engines: {node: '>=6'} - dev: true - - /queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - dev: true - - /redis@4.6.10: - resolution: {integrity: sha512-mmbyhuKgDiJ5TWUhiKhBssz+mjsuSI/lSZNPI9QvZOYzWvYGejtb+W3RlDDf8LD6Bdl5/mZeG8O1feUGhXTxEg==} - dependencies: - '@redis/bloom': 1.2.0(@redis/client@1.5.11) - '@redis/client': 1.5.11 - '@redis/graph': 1.1.0(@redis/client@1.5.11) - '@redis/json': 1.0.6(@redis/client@1.5.11) - '@redis/search': 1.1.5(@redis/client@1.5.11) - '@redis/time-series': 1.0.5(@redis/client@1.5.11) - dev: false - - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true - - /resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - dev: false - - /reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - dev: true - - /rimraf@3.0.2: - resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} - hasBin: true - dependencies: - glob: 7.2.3 - dev: true - - /run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - dependencies: - queue-microtask: 1.2.3 - dev: true - - /section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - dev: true - - /semver@7.5.4: - resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - - /shebang-command@2.0.0: - resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} - engines: {node: '>=8'} - dependencies: - shebang-regex: 3.0.0 - dev: true - - /shebang-regex@3.0.0: - resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} - engines: {node: '>=8'} - dev: true - - /slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - dev: true - - /source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - dependencies: - buffer-from: 1.1.2 - source-map: 0.6.1 - dev: false - - /source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - dev: false - - /sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - dev: true - - /strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - dependencies: - ansi-regex: 5.0.1 - dev: true - - /strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - dev: true - - /strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - dev: true - - /supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - dependencies: - has-flag: 4.0.0 - dev: true - - /text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} - dev: true - - /to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - dependencies: - is-number: 7.0.0 - dev: true - - /ts-api-utils@1.0.1(typescript@5.2.2): - resolution: {integrity: sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==} - engines: {node: '>=16.13.0'} - peerDependencies: - typescript: '>=4.2.0' - dependencies: - typescript: 5.2.2 - dev: true - - /ts-mixer@6.0.3: - resolution: {integrity: sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==} - dev: false - - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false - - /tsx@4.1.1: - resolution: {integrity: sha512-zyPn5BFMB0TB5kMLbYPNx4x/oL/oSlaecdKCv6WeJ0TeSEfx8RTJWjuB5TZ2dSewktgfBsBO/HNA9mrMWqLXMA==} - engines: {node: '>=18.0.0'} - hasBin: true - dependencies: - esbuild: 0.18.20 - get-tsconfig: 4.7.2 - source-map-support: 0.5.21 - optionalDependencies: - fsevents: 2.3.3 - dev: false - - /type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} - engines: {node: '>= 0.8.0'} - dependencies: - prelude-ls: 1.2.1 - dev: true - - /type-fest@0.20.2: - resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} - engines: {node: '>=10'} - dev: true - - /typescript@5.2.2: - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} - engines: {node: '>=14.17'} - hasBin: true - dev: true - - /undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - - /undici@5.27.2: - resolution: {integrity: sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==} - engines: {node: '>=14.0'} - dependencies: - '@fastify/busboy': 2.1.0 - dev: false - - /uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - dependencies: - punycode: 2.3.0 - dev: true - - /which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - dependencies: - isexe: 2.0.0 - dev: true - - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true - - /ws@8.14.2: - resolution: {integrity: sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - dev: false - - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - - /yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - dev: true diff --git a/renovate.json b/renovate.json index e9cca0b..d7a91d3 100644 --- a/renovate.json +++ b/renovate.json @@ -1,4 +1,4 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", "config:js-app"] + "extends": ["config:base", "config:recommended"] } diff --git a/src/_reupload.ts b/src/_reupload.ts deleted file mode 100644 index 43c9e73..0000000 --- a/src/_reupload.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - SlashCommandBuilder, - Routes, - PermissionFlagsBits, - type RESTGetAPIOAuth2CurrentApplicationResult, -} from 'discord.js'; -import { REST } from '@discordjs/rest'; -import { getTags } from './tags'; - -export const reuploadCommands = async () => { - const tags = await getTags(); - - const commands = [ - new SlashCommandBuilder() - .setName('ping') - .setDescription('Replies with pong!'), - new SlashCommandBuilder() - .setName('stars') - .setDescription('Returns GitHub stargazer count'), - new SlashCommandBuilder() - .setName('members') - .setDescription('Returns the number of members in the server'), - new SlashCommandBuilder() - .setName('tag') - .setDescription('Send a tag') - .addStringOption((option) => - option - .setName('name') - .setDescription('The tag name') - .setRequired(true) - .addChoices(...tags.map((b) => ({ name: b.name, value: b.name }))) - ) - .addUserOption((option) => - option - .setName('user') - .setDescription('The user to mention') - .setRequired(false) - ), - new SlashCommandBuilder() - .setName('modrinth') - .setDescription('Get info on a Modrinth project') - .addStringOption((option) => - option.setName('id').setDescription('The ID or slug').setRequired(true) - ), - new SlashCommandBuilder() - .setName('say') - .setDescription('Say something through the bot') - .addStringOption((option) => - option - .setName('content') - .setDescription('Just content?') - .setRequired(true) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) - .setDMPermission(false), - new SlashCommandBuilder().setName('joke').setDescription("it's a joke"), - new SlashCommandBuilder() - .setName('rory') - .setDescription('Gets a Rory photo!') - .addStringOption((option) => - option - .setName('id') - .setDescription('specify a Rory ID') - .setRequired(false) - ), - ].map((command) => command.toJSON()); - - const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!); - - const { id: appId } = (await rest.get( - Routes.oauth2CurrentApplication() - )) as RESTGetAPIOAuth2CurrentApplicationResult; - - await rest.put(Routes.applicationCommands(appId), { - body: commands, - }); - - console.log('Successfully registered application commands.'); -}; diff --git a/src/api/dadjoke.rs b/src/api/dadjoke.rs new file mode 100644 index 0000000..ac6ca5f --- /dev/null +++ b/src/api/dadjoke.rs @@ -0,0 +1,11 @@ +use super::{HttpClient, HttpClientExt}; + +use eyre::Result; + +const DADJOKE: &str = "https://icanhazdadjoke.com"; + +pub async fn get_joke(http: &HttpClient) -> Result { + let joke = http.get_request(DADJOKE).await?.text().await?; + + Ok(joke) +} diff --git a/src/api/github.rs b/src/api/github.rs new file mode 100644 index 0000000..18b533a --- /dev/null +++ b/src/api/github.rs @@ -0,0 +1,30 @@ +use eyre::{OptionExt, Result, WrapErr}; +use log::debug; +use octocrab::Octocrab; + +pub async fn get_latest_prism_version(octocrab: &Octocrab) -> Result { + debug!("Fetching the latest version of Prism Launcher"); + + let version = octocrab + .repos("PrismLauncher", "PrismLauncher") + .releases() + .get_latest() + .await? + .tag_name; + + Ok(version) +} + +pub async fn get_prism_stargazers_count(octocrab: &Octocrab) -> Result { + debug!("Fetching Prism Launcher's stargazer count"); + + let stargazers_count = octocrab + .repos("PrismLauncher", "PrismLauncher") + .get() + .await + .wrap_err("Couldn't fetch PrismLauncher/PrismLauncher!")? + .stargazers_count + .ok_or_eyre("Couldn't retrieve stargazers_coutn from GitHub!")?; + + Ok(stargazers_count) +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..cb1c7bf --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,36 @@ +use log::trace; +use reqwest::Response; + +pub mod dadjoke; +pub mod github; +pub mod paste_gg; +pub mod pluralkit; +pub mod prism_meta; +pub mod rory; + +pub type HttpClient = reqwest::Client; + +pub trait HttpClientExt { + // sadly i can't implement the actual Default trait :/ + fn default() -> Self; + async fn get_request(&self, url: &str) -> Result; +} + +impl HttpClientExt for HttpClient { + fn default() -> Self { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development"); + let user_agent = format!("refraction/{version}"); + reqwest::ClientBuilder::new() + .user_agent(user_agent) + .build() + .unwrap_or_default() + } + + async fn get_request(&self, url: &str) -> Result { + trace!("Making request to {url}"); + let resp = self.get(url).send().await?; + resp.error_for_status_ref()?; + + Ok(resp) + } +} diff --git a/src/api/paste_gg.rs b/src/api/paste_gg.rs new file mode 100644 index 0000000..a4004cb --- /dev/null +++ b/src/api/paste_gg.rs @@ -0,0 +1,55 @@ +use super::{HttpClient, HttpClientExt}; + +use eyre::{eyre, OptionExt, Result}; +use serde::{Deserialize, Serialize}; + +const PASTE_GG: &str = "https://api.paste.gg/v1"; +const PASTES: &str = "/pastes"; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] +pub enum Status { + #[serde(rename = "success")] + Success, + #[serde(rename = "error")] + Error, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Response { + pub status: Status, + pub result: Option>, + pub error: Option, + pub message: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Files { + pub id: String, + pub name: Option, +} + +pub async fn files_from(http: &HttpClient, id: &str) -> Result> { + let url = format!("{PASTE_GG}{PASTES}/{id}/files"); + let resp: Response = http.get_request(&url).await?.json().await?; + + if resp.status == Status::Error { + let message = resp + .error + .ok_or_eyre("Paste.gg gave us an error but with no message!")?; + + Err(eyre!(message)) + } else { + Ok(resp) + } +} + +pub async fn get_raw_file( + http: &HttpClient, + paste_id: &str, + file_id: &str, +) -> eyre::Result { + let url = format!("{PASTE_GG}{PASTES}/{paste_id}/files/{file_id}/raw"); + let text = http.get_request(&url).await?.text().await?; + + Ok(text) +} diff --git a/src/api/pluralkit.rs b/src/api/pluralkit.rs new file mode 100644 index 0000000..0b16793 --- /dev/null +++ b/src/api/pluralkit.rs @@ -0,0 +1,26 @@ +use super::{HttpClient, HttpClientExt}; + +use eyre::{Context, Result}; +use poise::serenity_prelude::{MessageId, UserId}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct Message { + pub sender: String, +} + +const PLURAL_KIT: &str = "https://api.pluralkit.me/v2"; +const MESSAGES: &str = "/messages"; + +pub async fn sender_from(http: &HttpClient, message_id: MessageId) -> Result { + let url = format!("{PLURAL_KIT}{MESSAGES}/{message_id}"); + let resp: Message = http.get_request(&url).await?.json().await?; + + let id: u64 = + resp.sender.parse().wrap_err_with(|| { + format!("Couldn't parse response from PluralKit as a UserId! Here's the response:\n{resp:#?}") + })?; + let sender = UserId::from(id); + + Ok(sender) +} diff --git a/src/api/prism_meta.rs b/src/api/prism_meta.rs new file mode 100644 index 0000000..f7efb0a --- /dev/null +++ b/src/api/prism_meta.rs @@ -0,0 +1,28 @@ +use super::{HttpClient, HttpClientExt}; + +use eyre::{OptionExt, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct MinecraftPackageJson { + pub format_version: u8, + pub name: String, + pub recommended: Vec, + pub uid: String, +} + +const META: &str = "https://meta.prismlauncher.org/v1"; +const MINECRAFT_PACKAGEJSON: &str = "/net.minecraft/package.json"; + +pub async fn latest_minecraft_version(http: &HttpClient) -> Result { + let url = format!("{META}{MINECRAFT_PACKAGEJSON}"); + let data: MinecraftPackageJson = http.get_request(&url).await?.json().await?; + + let version = data + .recommended + .first() + .ok_or_eyre("Couldn't find latest version of Minecraft!")?; + + Ok(version.clone()) +} diff --git a/src/api/rory.rs b/src/api/rory.rs new file mode 100644 index 0000000..3fffbbe --- /dev/null +++ b/src/api/rory.rs @@ -0,0 +1,28 @@ +use super::{HttpClient, HttpClientExt}; + +use eyre::{Context, Result}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Response { + pub id: u64, + pub url: String, + pub error: Option, +} + +const RORY: &str = "https://rory.cat"; +const PURR: &str = "/purr"; + +pub async fn get(http: &HttpClient, id: Option) -> Result { + let target = id.map(|id| id.to_string()).unwrap_or_default(); + let url = format!("{RORY}{PURR}/{target}"); + + let data: Response = http + .get_request(&url) + .await? + .json() + .await + .wrap_err("Couldn't parse the rory response!")?; + + Ok(data) +} diff --git a/src/commands/general/help.rs b/src/commands/general/help.rs new file mode 100644 index 0000000..bc6cb76 --- /dev/null +++ b/src/commands/general/help.rs @@ -0,0 +1,22 @@ +use crate::{Context, Error}; + +use log::trace; +use poise::samples::HelpConfiguration; + +/// View the help menu +#[poise::command(slash_command, prefix_command, track_edits = true)] +pub async fn help( + ctx: Context<'_>, + #[description = "Provide information about a specific command"] command: Option, +) -> Result<(), Error> { + trace!("Running help command"); + + let configuration = HelpConfiguration { + extra_text_at_bottom: "Use /help for more information about a specific command!", + ..Default::default() + }; + + poise::builtins::help(ctx, command.as_deref(), configuration).await?; + + Ok(()) +} diff --git a/src/commands/general/joke.rs b/src/commands/general/joke.rs new file mode 100644 index 0000000..e08282e --- /dev/null +++ b/src/commands/general/joke.rs @@ -0,0 +1,16 @@ +use crate::{api::dadjoke, Context, Error}; + +use eyre::Result; +use log::trace; + +/// It's a joke +#[poise::command(slash_command, prefix_command, track_edits = true)] +pub async fn joke(ctx: Context<'_>) -> Result<(), Error> { + trace!("Running joke command"); + + ctx.defer().await?; + let joke = dadjoke::get_joke(&ctx.data().http_client).await?; + ctx.say(joke).await?; + + Ok(()) +} diff --git a/src/commands/general/members.rs b/src/commands/general/members.rs new file mode 100644 index 0000000..0476972 --- /dev/null +++ b/src/commands/general/members.rs @@ -0,0 +1,37 @@ +use crate::{consts::Colors, Context, Error}; + +use eyre::{eyre, Context as _, OptionExt}; +use log::trace; +use poise::serenity_prelude::CreateEmbed; +use poise::CreateReply; + +/// Returns the number of members in the server +#[poise::command(slash_command, prefix_command, guild_only = true, track_edits = true)] +pub async fn members(ctx: Context<'_>) -> Result<(), Error> { + trace!("Running members command"); + + ctx.defer().await?; + + let guild_id = ctx.guild_id().ok_or_eyre("Couldn't get guild ID!")?; + let guild = ctx + .http() + .get_guild_with_counts(guild_id) + .await + .wrap_err_with(|| format!("Couldn't fetch guild {guild_id} with counts!"))?; + + let member_count = guild + .approximate_member_count + .ok_or_else(|| eyre!("Couldn't get member count for guild {guild_id}!"))?; + let online_count = guild + .approximate_presence_count + .ok_or_else(|| eyre!("Couldn't get online count for guild {guild_id}!"))?; + + let embed = CreateEmbed::new() + .title(format!("{member_count} total members!",)) + .description(format!("{online_count} online members",)) + .color(Colors::Blue); + let reply = CreateReply::default().embed(embed); + + ctx.send(reply).await?; + Ok(()) +} diff --git a/src/commands/general/mod.rs b/src/commands/general/mod.rs new file mode 100644 index 0000000..7e2d92f --- /dev/null +++ b/src/commands/general/mod.rs @@ -0,0 +1,8 @@ +pub mod help; +pub mod joke; +pub mod members; +pub mod ping; +pub mod rory; +pub mod say; +pub mod stars; +pub mod tag; diff --git a/src/commands/general/ping.rs b/src/commands/general/ping.rs new file mode 100644 index 0000000..09544ee --- /dev/null +++ b/src/commands/general/ping.rs @@ -0,0 +1,11 @@ +use crate::{Context, Error}; + +use log::trace; + +/// Replies with pong! +#[poise::command(slash_command, prefix_command, track_edits = true, ephemeral)] +pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { + trace!("Running ping command!"); + ctx.say("Pong!").await?; + Ok(()) +} diff --git a/src/commands/general/rory.rs b/src/commands/general/rory.rs new file mode 100644 index 0000000..f02783d --- /dev/null +++ b/src/commands/general/rory.rs @@ -0,0 +1,37 @@ +use crate::{api::rory, Context, Error}; + +use log::trace; +use poise::serenity_prelude::{CreateEmbed, CreateEmbedFooter}; +use poise::CreateReply; + +/// Gets a Rory photo! +#[poise::command(slash_command, prefix_command, track_edits = true)] +pub async fn rory( + ctx: Context<'_>, + #[description = "specify a Rory ID"] id: Option, +) -> Result<(), Error> { + trace!("Running rory command"); + + ctx.defer().await?; + + let rory = rory::get(&ctx.data().http_client, id).await?; + + let embed = { + let embed = CreateEmbed::new(); + if let Some(error) = rory.error { + embed.title("Error!").description(error) + } else { + let footer = CreateEmbedFooter::new(format!("ID {}", rory.id)); + embed + .title("Rory :3") + .url(&rory.url) + .image(rory.url) + .footer(footer) + } + }; + + let reply = CreateReply::default().embed(embed); + ctx.send(reply).await?; + + Ok(()) +} diff --git a/src/commands/general/say.rs b/src/commands/general/say.rs new file mode 100644 index 0000000..b873afd --- /dev/null +++ b/src/commands/general/say.rs @@ -0,0 +1,37 @@ +use crate::{utils, Context, Error}; + +use log::trace; +use poise::serenity_prelude::{CreateEmbed, CreateMessage}; + +/// Say something through the bot +#[poise::command( + slash_command, + ephemeral, + default_member_permissions = "MODERATE_MEMBERS", + required_permissions = "MODERATE_MEMBERS", + guild_only +)] +pub async fn say( + ctx: Context<'_>, + #[description = "the message content"] content: String, +) -> Result<(), Error> { + let channel = ctx.channel_id(); + channel.say(ctx, &content).await?; + ctx.say("I said what you said!").await?; + + if let Some(channel_id) = ctx.data().config.discord.channels.log_channel_id { + let author = utils::embed_author_from_user(ctx.author()); + + let embed = CreateEmbed::default() + .title("Say command used!") + .description(content) + .author(author); + + let message = CreateMessage::new().embed(embed); + channel_id.send_message(ctx, message).await?; + } else { + trace!("Not sending /say log as no channel is set"); + } + + Ok(()) +} diff --git a/src/commands/general/stars.rs b/src/commands/general/stars.rs new file mode 100644 index 0000000..603f435 --- /dev/null +++ b/src/commands/general/stars.rs @@ -0,0 +1,36 @@ +use crate::{api, consts::Colors, Context, Error}; + +use log::trace; +use poise::serenity_prelude::CreateEmbed; +use poise::CreateReply; + +/// Returns GitHub stargazer count +#[poise::command(slash_command, prefix_command, track_edits = true)] +pub async fn stars(ctx: Context<'_>) -> Result<(), Error> { + trace!("Running stars command"); + let octocrab = &ctx.data().octocrab; + + ctx.defer().await?; + + let count = if let Some(storage) = &ctx.data().storage { + if let Ok(count) = storage.launcher_stargazer_count().await { + count + } else { + let count = api::github::get_prism_stargazers_count(octocrab).await?; + storage.cache_launcher_stargazer_count(count).await?; + count + } + } else { + trace!("Not caching launcher stargazer count, as we're running without a storage backend"); + api::github::get_prism_stargazers_count(octocrab).await? + }; + + let embed = CreateEmbed::new() + .title(format!("⭐ {count} total stars!")) + .color(Colors::Yellow); + let reply = CreateReply::default().embed(embed); + + ctx.send(reply).await?; + + Ok(()) +} diff --git a/src/commands/general/tag.rs b/src/commands/general/tag.rs new file mode 100644 index 0000000..3cfe1a1 --- /dev/null +++ b/src/commands/general/tag.rs @@ -0,0 +1,90 @@ +#![allow(non_camel_case_types, clippy::upper_case_acronyms)] +use crate::{consts::Colors, tags::Tag, Context, Error}; +use std::env; +use std::str::FromStr; +use std::sync::OnceLock; + +use eyre::eyre; +use log::trace; +use poise::serenity_prelude::{Color, CreateEmbed, User}; +use poise::CreateReply; + +include!(concat!(env!("OUT_DIR"), "/generated.rs")); +fn tags() -> &'static Vec { + static TAGS: OnceLock> = OnceLock::new(); + TAGS.get_or_init(|| serde_json::from_str(env!("TAGS")).unwrap()) +} + +/// Send a tag +#[poise::command( + slash_command, + prefix_command, + track_edits = true, + help_text_fn = help +)] +pub async fn tag( + ctx: Context<'_>, + #[description = "the tag to send"] name: Choice, + #[description = "a user to mention"] user: Option, +) -> Result<(), Error> { + trace!("Running tag command"); + + let tag_id = name.as_str(); + let tag = tags() + .iter() + .find(|t| t.id == tag_id) + .ok_or_else(|| eyre!("Tried to get non-existent tag: {tag_id}"))?; + + let frontmatter = &tag.frontmatter; + + let embed = { + let mut e = CreateEmbed::new(); + + if let Some(color) = &frontmatter.color { + let color = Colors::from_str(color.as_str()) + .map(Color::from) + .unwrap_or_default(); + + e = e.color(color); + } + + if let Some(image) = &frontmatter.image { + e = e.image(image); + } + + if let Some(fields) = &frontmatter.fields { + for field in fields { + e = e.field(&field.name, &field.value, field.inline); + } + } + + e = e.title(&frontmatter.title); + e = e.description(&tag.content); + + e + }; + + let reply = { + let mut r = CreateReply::default(); + + if let Some(user) = user { + r = r.content(format!("<@{}>", user.id)); + } + + r.embed(embed) + }; + + ctx.send(reply).await?; + + Ok(()) +} + +fn help() -> String { + let tag_list = tags() + .iter() + .map(|tag| format!("`{}`", tag.id)) + .collect::>() + .join(", "); + + format!("Available tags: {tag_list}") +} diff --git a/src/commands/joke.ts b/src/commands/joke.ts deleted file mode 100644 index b0f9a34..0000000 --- a/src/commands/joke.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CacheType, ChatInputCommandInteraction } from 'discord.js'; - -export const jokeCommand = async ( - i: ChatInputCommandInteraction -) => { - await i.deferReply(); - const joke = await fetch('https://icanhazdadjoke.com', { - headers: { Accept: 'text/plain' }, - }).then((r) => r.text()); - await i.editReply(joke); -}; diff --git a/src/commands/members.ts b/src/commands/members.ts deleted file mode 100644 index 1e7bde9..0000000 --- a/src/commands/members.ts +++ /dev/null @@ -1,24 +0,0 @@ -import type { CacheType, ChatInputCommandInteraction } from 'discord.js'; - -import { COLORS } from '../constants'; - -export const membersCommand = async ( - i: ChatInputCommandInteraction -) => { - await i.deferReply(); - - const memes = await i.guild?.members.fetch().then((r) => r.toJSON()); - if (!memes) return; - - await i.editReply({ - embeds: [ - { - title: `${memes.length} total members!`, - description: `${ - memes.filter((m) => m.presence?.status !== 'offline').length - } online members`, - color: COLORS.blue, - }, - ], - }); -}; diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..2292afd --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,47 @@ +use crate::{Data, Error}; + +mod general; +mod moderation; + +macro_rules! command { + ($module: ident, $name: ident) => { + $module::$name::$name() + }; + + ($module: ident, $name: ident, $func: ident) => { + $module::$name::$func() + }; +} + +macro_rules! module_macro { + ($module: ident) => { + macro_rules! $module { + ($name: ident) => { + command!($module, $name) + }; + + ($name: ident, $func: ident) => { + command!($module, $name, $func) + }; + } + }; +} + +module_macro!(general); +module_macro!(moderation); + +pub type Command = poise::Command; + +pub fn all() -> Vec { + vec![ + general!(help), + general!(joke), + general!(members), + general!(ping), + general!(rory), + general!(say), + general!(stars), + general!(tag), + moderation!(set_welcome), + ] +} diff --git a/src/commands/moderation/mod.rs b/src/commands/moderation/mod.rs new file mode 100644 index 0000000..d5578a0 --- /dev/null +++ b/src/commands/moderation/mod.rs @@ -0,0 +1 @@ +pub mod set_welcome; diff --git a/src/commands/moderation/set_welcome.rs b/src/commands/moderation/set_welcome.rs new file mode 100644 index 0000000..be35ac0 --- /dev/null +++ b/src/commands/moderation/set_welcome.rs @@ -0,0 +1,198 @@ +use std::{fmt::Write, str::FromStr}; + +use crate::{api::HttpClientExt, utils, Context, Error}; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::{ + futures::TryStreamExt, Attachment, CreateActionRow, CreateButton, CreateEmbed, CreateMessage, + Mentionable, Message, ReactionType, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct WelcomeEmbed { + title: String, + description: Option, + url: Option, + hex_color: Option, + image: Option, +} + +impl From for CreateMessage { + fn from(val: WelcomeEmbed) -> Self { + let mut embed = CreateEmbed::new(); + + embed = embed.title(val.title); + if let Some(description) = val.description { + embed = embed.description(description); + } + + if let Some(url) = val.url { + embed = embed.url(url); + } + + if let Some(color) = val.hex_color { + let hex = i32::from_str_radix(&color, 16).unwrap(); + embed = embed.color(hex); + } + + if let Some(image) = val.image { + embed = embed.image(image); + } + + Self::new().embed(embed) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct WelcomeRole { + title: String, + id: u64, + emoji: Option, +} + +impl From for CreateButton { + fn from(value: WelcomeRole) -> Self { + let mut button = Self::new(value.id.to_string()).label(value.title); + if let Some(emoji) = value.emoji { + button = button.emoji(ReactionType::from_str(&emoji).unwrap()); + } + + button + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct WelcomeRoleCategory { + title: String, + description: Option, + roles: Vec, +} + +impl From for CreateMessage { + fn from(value: WelcomeRoleCategory) -> Self { + let mut content = format!("**{}**", value.title); + if let Some(description) = value.description { + write!(content, "\n{description}").ok(); + } + + let buttons: Vec = value + .roles + .iter() + .map(|role| CreateButton::from(role.clone())) + .collect(); + + let components = vec![CreateActionRow::Buttons(buttons)]; + Self::new().content(content).components(components) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +struct WelcomeLayout { + embeds: Vec, + messages: Vec, + roles: Vec, +} + +/// Sets your welcome channel info +#[poise::command( + slash_command, + guild_only, + ephemeral, + default_member_permissions = "MANAGE_GUILD", + required_permissions = "MANAGE_GUILD" +)] +pub async fn set_welcome( + ctx: Context<'_>, + #[description = "A file to use"] file: Option, + #[description = "A URL for a file to use"] url: Option, +) -> Result<(), Error> { + trace!("Running set_welcome command!"); + + let configured_channels = ctx.data().config.discord.channels; + let Some(channel_id) = configured_channels.welcome_channel_id else { + ctx.say("You don't have a welcome channel ID set, so I can't do anything :(") + .await?; + return Ok(()); + }; + + ctx.defer_ephemeral().await?; + + // download attachment from discord or URL + let file = if let Some(attachment) = file { + let Some(content_type) = &attachment.content_type else { + return Err("Welcome channel attachment was sent without a content type!".into()); + }; + + if !content_type.starts_with("application/json;") { + trace!("Not attempting to read non-json content type {content_type}"); + ctx.say("Invalid file! Please only send json").await?; + return Ok(()); + } + + let downloaded = attachment.download().await?; + String::from_utf8(downloaded)? + } else if let Some(url) = url { + ctx.data() + .http_client + .get_request(&url) + .await? + .text() + .await? + } else { + ctx.say("A text file or URL must be provided!").await?; + return Ok(()); + }; + + // parse and create messages from file + let welcome_layout: WelcomeLayout = serde_json::from_str(&file)?; + let embed_messages: Vec = welcome_layout + .embeds + .iter() + .map(|e| CreateMessage::from(e.clone())) + .collect(); + let roles_messages: Vec = welcome_layout + .roles + .iter() + .map(|c| CreateMessage::from(c.clone())) + .collect(); + + // clear previous messages + let prev_messages: Vec = channel_id.messages_iter(ctx).try_collect().await?; + channel_id.delete_messages(ctx, prev_messages).await?; + + // send our new ones + for embed in embed_messages { + channel_id.send_message(ctx, embed).await?; + } + + for message in roles_messages { + channel_id.send_message(ctx, message).await?; + } + + for message in welcome_layout.messages { + channel_id.say(ctx, message).await?; + } + + if let Some(log_channel) = configured_channels.log_channel_id { + let author = utils::embed_author_from_user(ctx.author()); + let embed = CreateEmbed::new() + .title("set_welcome command used!") + .author(author); + let message = CreateMessage::new().embed(embed); + + log_channel.send_message(ctx, message).await?; + } else { + trace!("Not sending /set_welcome log as no channel is set"); + } + + ctx.reply(format!("Updated {}!", channel_id.mention())) + .await?; + + Ok(()) +} diff --git a/src/commands/modrinth.ts b/src/commands/modrinth.ts deleted file mode 100644 index ba1291d..0000000 --- a/src/commands/modrinth.ts +++ /dev/null @@ -1,124 +0,0 @@ -type Side = 'required' | 'optional' | 'unsupported'; - -export interface ModrinthProject { - slug: string; - title: string; - description: string; - categories: string[]; - client_side: Side; - server_side: Side; - project_type: 'mod' | 'modpack'; - downloads: number; - icon_url: string | null; - id: string; - team: string; -} - -import { - EmbedBuilder, - type CacheType, - type ChatInputCommandInteraction, -} from 'discord.js'; - -import { COLORS } from '../constants'; - -export const modrinthCommand = async ( - i: ChatInputCommandInteraction -) => { - await i.deferReply(); - - const { value: id } = i.options.get('id') ?? { value: null }; - - if (!id || typeof id !== 'string') { - await i.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle('Error!') - .setDescription('You need to provide a valid mod ID!') - .setColor(COLORS.red), - ], - }); - - return; - } - - const res = await fetch('https://api.modrinth.com/v2/project/' + id); - - if (!res.ok) { - await i.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle('Error!') - .setDescription('Not found!') - .setColor(COLORS.red), - ], - }); - - setTimeout(() => { - i.deleteReply(); - }, 3000); - - return; - } - - const data = (await res.json()) as - | ModrinthProject - | { error: string; description: string }; - - if ('error' in data) { - console.error(data); - - await i.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle('Error!') - .setDescription(`\`${data.error}\` ${data.description}`) - .setColor(COLORS.red), - ], - }); - - setTimeout(() => { - i.deleteReply(); - }, 3000); - - return; - } - - await i.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle(data.title) - .setDescription(data.description) - .setThumbnail(data.icon_url) - .setURL(`https://modrinth.com/project/${data.slug}`) - .setFields([ - { - name: 'Categories', - value: data.categories.join(', '), - inline: true, - }, - { - name: 'Project type', - value: data.project_type, - inline: true, - }, - { - name: 'Downloads', - value: data.downloads.toString(), - inline: true, - }, - { - name: 'Client', - value: data.client_side, - inline: true, - }, - { - name: 'Server', - value: data.server_side, - inline: true, - }, - ]) - .setColor(COLORS.green), - ], - }); -}; diff --git a/src/commands/rory.ts b/src/commands/rory.ts deleted file mode 100644 index 0a1026e..0000000 --- a/src/commands/rory.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { CacheType, ChatInputCommandInteraction } from 'discord.js'; -import { EmbedBuilder } from 'discord.js'; - -export interface RoryResponse { - /** - * The ID of this Rory - */ - id: number; - /** - * The URL to the image of this Rory - */ - url: string; - /** - * When error :( - */ - error: string | undefined; -} - -export const roryCommand = async ( - i: ChatInputCommandInteraction -) => { - await i.deferReply(); - - const { value: id } = i.options.get('id') ?? { value: '' }; - - const rory: RoryResponse = await fetch(`https://rory.cat/purr/${id}`, { - headers: { Accept: 'application/json' }, - }).then((r) => r.json()); - - if (rory.error) { - await i.editReply({ - embeds: [ - new EmbedBuilder().setTitle('Error!').setDescription(rory.error), - ], - }); - - return; - } - - await i.editReply({ - embeds: [ - new EmbedBuilder() - .setTitle('Rory :3') - .setURL(`https://rory.cat/id/${rory.id}`) - .setImage(rory.url) - .setFooter({ - text: `ID ${rory.id}`, - }), - ], - }); -}; diff --git a/src/commands/say.ts b/src/commands/say.ts deleted file mode 100644 index fd3af32..0000000 --- a/src/commands/say.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - CacheType, - ChatInputCommandInteraction, - EmbedBuilder, -} from 'discord.js'; - -export const sayCommand = async ( - interaction: ChatInputCommandInteraction -) => { - if (!interaction.guild || !interaction.channel) return; - - const content = interaction.options.getString('content', true); - await interaction.deferReply({ ephemeral: true }); - const message = await interaction.channel.send(content); - await interaction.editReply('I said what you said!'); - - if (process.env.SAY_LOGS_CHANNEL) { - const logsChannel = await interaction.guild.channels.fetch( - process.env.SAY_LOGS_CHANNEL - ); - - if (!logsChannel?.isTextBased()) return; - - await logsChannel.send({ - embeds: [ - new EmbedBuilder() - .setTitle('Say command used') - .setDescription(content) - .setAuthor({ - name: interaction.user.tag, - iconURL: interaction.user.avatarURL() ?? undefined, - }) - .setURL(message.url), - ], - }); - } -}; diff --git a/src/commands/stars.ts b/src/commands/stars.ts deleted file mode 100644 index 9eea2ab..0000000 --- a/src/commands/stars.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { CacheType, ChatInputCommandInteraction } from 'discord.js'; -import { COLORS } from '../constants'; - -export const starsCommand = async ( - i: ChatInputCommandInteraction -) => { - await i.deferReply(); - - const count = await fetch( - 'https://api.github.com/repos/PrismLauncher/PrismLauncher' - ) - .then((r) => r.json() as Promise<{ stargazers_count: number }>) - .then((j) => j.stargazers_count); - - await i.editReply({ - embeds: [ - { - title: `⭐ ${count} total stars!`, - color: COLORS.yellow, - }, - ], - }); -}; diff --git a/src/commands/tags.ts b/src/commands/tags.ts deleted file mode 100644 index 870e452..0000000 --- a/src/commands/tags.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - type ChatInputCommandInteraction, - type CacheType, - EmbedBuilder, -} from 'discord.js'; -import { getTags } from '../tags'; - -export const tagsCommand = async ( - i: ChatInputCommandInteraction -) => { - const tags = await getTags(); - const tagName = i.options.getString('name', true); - const mention = i.options.getUser('user', false); - - const tag = tags.find( - (tag) => tag.name === tagName || tag.aliases?.includes(tagName) - ); - - if (!tag) { - await i.reply({ - content: `Tag \`${tagName}\` does not exist.`, - ephemeral: true, - }); - return; - } - - const embed = new EmbedBuilder(); - embed.setTitle(tag.title ?? tag.name); - embed.setDescription(tag.content); - if (tag.color) embed.setColor(tag.color); - if (tag.image) embed.setImage(tag.image); - if (tag.fields) embed.setFields(tag.fields); - - await i.reply({ - content: mention ? `<@${mention.id}> ` : undefined, - embeds: [embed], - }); -}; diff --git a/src/config/bot.rs b/src/config/bot.rs new file mode 100644 index 0000000..c34c5b0 --- /dev/null +++ b/src/config/bot.rs @@ -0,0 +1,24 @@ +use log::{info, warn}; + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub redis_url: Option, +} + +impl Config { + pub fn new(redis_url: Option) -> Self { + Self { redis_url } + } + + pub fn from_env() -> Self { + let redis_url = std::env::var("BOT_REDIS_URL").ok(); + + if let Some(url) = &redis_url { + info!("Redis URL is {url}"); + } else { + warn!("BOT_REDIS_URL is empty; features requiring storage will be disabled."); + } + + Self::new(redis_url) + } +} diff --git a/src/config/discord.rs b/src/config/discord.rs new file mode 100644 index 0000000..27782b1 --- /dev/null +++ b/src/config/discord.rs @@ -0,0 +1,60 @@ +use std::str::FromStr; + +use log::{info, warn}; +use poise::serenity_prelude::ChannelId; + +#[derive(Clone, Copy, Debug, Default)] +pub struct RefractionChannels { + pub log_channel_id: Option, + pub welcome_channel_id: Option, +} + +#[derive(Clone, Debug, Default)] +pub struct Config { + pub channels: RefractionChannels, +} + +impl RefractionChannels { + pub fn new(log_channel_id: Option, welcome_channel_id: Option) -> Self { + Self { + log_channel_id, + welcome_channel_id, + } + } + + pub fn new_from_env() -> Self { + let log_channel_id = Self::get_channel_from_env("DISCORD_LOG_CHANNEL_ID"); + if let Some(channel_id) = log_channel_id { + info!("Log channel is {channel_id}"); + } else { + warn!("DISCORD_LOG_CHANNEL_ID is empty; this will disable logging in your server."); + } + + let welcome_channel_id = Self::get_channel_from_env("DISCORD_WELCOME_CHANNEL_ID"); + if let Some(channel_id) = welcome_channel_id { + info!("Welcome channel is {channel_id}"); + } else { + warn!("DISCORD_WELCOME_CHANNEL_ID is empty; this will disable welcome channel features in your server"); + } + + Self::new(log_channel_id, welcome_channel_id) + } + + fn get_channel_from_env(var: &str) -> Option { + std::env::var(var) + .ok() + .and_then(|env_var| ChannelId::from_str(&env_var).ok()) + } +} + +impl Config { + pub fn new(channels: RefractionChannels) -> Self { + Self { channels } + } + + pub fn from_env() -> Self { + let channels = RefractionChannels::new_from_env(); + + Self::new(channels) + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..3507670 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,24 @@ +mod bot; +mod discord; + +#[derive(Debug, Clone, Default)] +pub struct Config { + pub bot: bot::Config, + pub discord: discord::Config, +} + +impl Config { + pub fn new(bot_config: bot::Config, discord_config: discord::Config) -> Self { + Self { + bot: bot_config, + discord: discord_config, + } + } + + pub fn new_from_env() -> Self { + let bot = bot::Config::from_env(); + let discord = discord::Config::from_env(); + + Self::new(bot, discord) + } +} diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index fb22a8f..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const ETA_REGEX = /\beta\b/i; -export const ETA_MESSAGES = [ - 'Sometime', - 'Some day', - 'Not far', - 'The future', - 'Never', - 'Perhaps tomorrow?', - 'There are no ETAs', - 'No', - 'Nah', - 'Yes', - 'Yas', - 'Next month', - 'Next year', - 'Next week', - 'In Prism Launcher 2.0.0', - 'At the appropriate juncture, in due course, in the fullness of time', -]; - -export const COLORS = { - red: 0xef4444, - green: 0x22c55e, - blue: 0x60a5fa, - yellow: 0xfde047, - orange: 0xfb923c, -} as { [key: string]: number }; diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..3b3f6e4 --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,46 @@ +#![allow(clippy::unreadable_literal)] +use std::str::FromStr; + +use poise::serenity_prelude::Colour; + +const BLUE: u32 = 0x60A5FA; +const GREEN: u32 = 0x22C55E; +const ORANGE: u32 = 0xFB923C; +const RED: u32 = 0xEF4444; +const YELLOW: u32 = 0xFDE047; + +#[derive(Clone, Copy, Debug, Default)] +pub enum Colors { + Blue, + #[default] + Green, + Orange, + Red, + Yellow, +} + +impl From for Colour { + fn from(value: Colors) -> Self { + Self::from(match &value { + Colors::Blue => BLUE, + Colors::Green => GREEN, + Colors::Orange => ORANGE, + Colors::Red => RED, + Colors::Yellow => YELLOW, + }) + } +} + +impl FromStr for Colors { + type Err = (); + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "blue" => Ok(Self::Blue), + "green" => Ok(Self::Green), + "orange" => Ok(Self::Orange), + "red" => Ok(Self::Red), + "yellow" => Ok(Self::Yellow), + _ => Err(()), + } + } +} diff --git a/src/handlers/error.rs b/src/handlers/error.rs new file mode 100644 index 0000000..58d7a86 --- /dev/null +++ b/src/handlers/error.rs @@ -0,0 +1,98 @@ +use crate::{consts::Colors, Data, Error}; + +use std::fmt::Write; + +use log::error; +use poise::serenity_prelude::{CreateEmbed, Timestamp}; +use poise::{CreateReply, FrameworkError}; + +// getchoo: i like writeln! and don't like +macro_rules! writelne { + ($dst:expr, $($arg:tt)*) => { + if let Err(why) = writeln!($dst, $($arg)*) { + error!("We somehow cannot write to what should be on the heap. What are you using this macro with? Anyways, here's the error:\n{why:#?}"); + } + } +} + +pub async fn handle(error: FrameworkError<'_, Data, Error>) { + match error { + FrameworkError::Setup { + error, framework, .. + } => { + error!("Error setting up client! Bailing out"); + framework.shard_manager().shutdown_all().await; + + panic!("{error}") + } + + FrameworkError::Command { error, ctx, .. } => { + error!("Error in command {}:\n{error:?}", ctx.command().name); + + let embed = CreateEmbed::new() + .title("Something went wrong!") + .description("oopsie") + .timestamp(Timestamp::now()) + .color(Colors::Red); + + let reply = CreateReply::default().embed(embed); + + ctx.send(reply).await.ok(); + } + + FrameworkError::EventHandler { + error, + ctx: _, + event, + framework: _, + .. + } => { + error!( + "Error while handling event {}:\n{error:?}", + event.snake_case_name() + ); + } + + FrameworkError::ArgumentParse { + error, input, ctx, .. + } => { + let mut response = String::new(); + + if let Some(input) = input { + writelne!( + &mut response, + "**Cannot parse `{input}` as argument: {error}**\n" + ); + } else { + writelne!(&mut response, "**{error}**\n"); + } + + if let Some(help_text) = ctx.command().help_text.as_ref() { + writelne!(&mut response, "{help_text}\n"); + } + + if ctx.command().invoke_on_edit { + writelne!( + &mut response, + "**Tip:** Edit your message to update the response." + ); + } + + writelne!( + &mut response, + "For more information, refer to /help {}.", + ctx.command().name + ); + + if let Err(why) = ctx.say(response).await { + error!("Unhandled error displaying ArgumentParse error\n{why:#?}"); + } + } + + error => { + if let Err(e) = poise::builtins::on_error(error).await { + error!("Unhandled error occurred:\n{e:#?}"); + } + } + } +} diff --git a/src/handlers/event/analyze_logs/issues.rs b/src/handlers/event/analyze_logs/issues.rs new file mode 100644 index 0000000..00b0e5e --- /dev/null +++ b/src/handlers/event/analyze_logs/issues.rs @@ -0,0 +1,266 @@ +use crate::{api, Data}; + +use std::sync::OnceLock; + +use eyre::Result; +use log::trace; +use regex::Regex; + +pub type Issue = Option<(String, String)>; + +pub async fn find(log: &str, data: &Data) -> Result> { + trace!("Checking log for issues"); + + let issues = [ + fabric_internal, + flatpak_nvidia, + forge_java, + intel_hd, + java_option, + lwjgl_2_java_9, + macos_ns, + oom, + optinotfine, + pre_1_12_native_transport_java_9, + wrong_java, + ]; + + let mut res: Vec<(String, String)> = issues.iter().filter_map(|issue| issue(log)).collect(); + + if let Some(issues) = outdated_launcher(log, data).await? { + res.push(issues); + } + + Ok(res) +} + +fn fabric_internal(log: &str) -> Issue { + const CLASS_NOT_FOUND: &str = "Caused by: java.lang.ClassNotFoundException: "; + + let issue = ( + "Fabric Internal Access".to_string(), + "The mod you are using is using fabric internals that are not meant \ + to be used by anything but the loader itself. + Those mods break both on Quilt and with fabric updates. + If you're using fabric, downgrade your fabric loader could work, \ + on Quilt you can try updating to the latest beta version, \ + but there's nothing much to do unless the mod author stops using them." + .to_string(), + ); + + let errors = [ + &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.impl"), + &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.mixin"), + &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.impl"), + &format!("{CLASS_NOT_FOUND}net.fabricmc.fabric.loader.mixin"), + "org.quiltmc.loader.impl.FormattedException: java.lang.NoSuchMethodError:", + ]; + + let found = errors.iter().any(|e| log.contains(e)); + found.then_some(issue) +} + +fn flatpak_nvidia(log: &str) -> Issue { + let issue = ( + "Outdated Nvidia Flatpak Driver".to_string(), + "The Nvidia driver for flatpak is outdated. + Please run `flatpak update` to fix this issue. \ + If that does not solve it, \ + please wait until the driver is added to Flathub and run it again." + .to_string(), + ); + + let found = log.contains("org.lwjgl.LWJGLException: Could not choose GLX13 config") + || log.contains("GLFW error 65545: GLX: Failed to find a suitable GLXFBConfig"); + + found.then_some(issue) +} + +fn forge_java(log: &str) -> Issue { + let issue = ( + "Forge Java Bug".to_string(), + "Old versions of Forge crash with Java 8u321+. + To fix this, update forge to the latest version via the Versions tab + (right click on Forge, click Change Version, and choose the latest one) + Alternatively, you can download 8u312 or lower. \ + See [archive](https://github.com/adoptium/temurin8-binaries/releases/tag/jdk8u312-b07)" + .to_string(), + ); + + let found = log.contains("java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.(Ljava/util/jar/Manifest;)V"); + found.then_some(issue) +} + +fn intel_hd(log: &str) -> Issue { + let issue = + ( + "Intel HD Windows 10".to_string(), + "Your drivers don't support windows 10 officially + See https://prismlauncher.org/wiki/getting-started/installing-java/#a-note-about-intel-hd-20003000-on-windows-10 for more info".to_string() + ); + + let found = log.contains("org.lwjgl.LWJGLException: Pixel format not accelerated"); + found.then_some(issue) +} + +fn java_option(log: &str) -> Issue { + static VM_OPTION_REGEX: OnceLock = OnceLock::new(); + static UNRECOGNIZED_OPTION_REGEX: OnceLock = OnceLock::new(); + + let vm_option = + VM_OPTION_REGEX.get_or_init(|| Regex::new(r"Unrecognized VM option '(.+)'[\r\n]").unwrap()); + let unrecognized_option = UNRECOGNIZED_OPTION_REGEX + .get_or_init(|| Regex::new(r"Unrecognized option: (.+)[\r\n]").unwrap()); + + if let Some(captures) = vm_option.captures(log) { + let title = if &captures[1] == "UseShenandoahGC" { + "Wrong Java Arguments" + } else { + "Java 8 and below don't support ShenandoahGC" + }; + return Some(( + title.to_string(), + format!("Remove `-XX:{}` from your Java arguments", &captures[1]), + )); + } + + if let Some(captures) = unrecognized_option.captures(log) { + return Some(( + "Wrong Java Arguments".to_string(), + format!("Remove `{}` from your Java arguments", &captures[1]), + )); + } + + None +} + +fn lwjgl_2_java_9(log: &str) -> Issue { + let issue = ( + "Linux: crash with pre-1.13 and Java 9+".to_string(), + "Using pre-1.13 (which uses LWJGL 2) with Java 9 or later usually causes a crash. \ + Switching to Java 8 or below will fix your issue. + Alternatively, you can use [Temurin](https://adoptium.net/temurin/releases). \ + However, multiplayer will not work in versions from 1.8 to 1.11. + For more information, type `/tag java`." + .to_string(), + ); + + let found = log.contains("check_match: Assertion `version->filename == NULL || ! _dl_name_match_p (version->filename, map)' failed!"); + found.then_some(issue) +} + +fn macos_ns(log: &str) -> Issue { + let issue = ( + "MacOS NSInternalInconsistencyException".to_string(), + "You need to downgrade your Java 8 version. See https://prismlauncher.org/wiki/getting-started/installing-java/#older-minecraft-on-macos".to_string() +); + + let found = + log.contains("Terminating app due to uncaught exception 'NSInternalInconsistencyException"); + found.then_some(issue) +} + +fn oom(log: &str) -> Issue { + let issue = ( + "Out of Memory".to_string(), + "Allocating more RAM to your instance could help prevent this crash.".to_string(), + ); + + let found = log.contains("java.lang.OutOfMemoryError"); + found.then_some(issue) +} + +fn optinotfine(log: &str) -> Issue { + let issue = ( + "Potential OptiFine Incompatibilities".to_string(), + "OptiFine is known to cause problems when paired with other mods. \ + Try to disable OptiFine and see if the issue persists. + Check `/tag optifine` for more info & some typically more compatible alternatives you can use." + .to_string(), + ); + + let found = log.contains("[✔] OptiFine_") || log.contains("[✔] optifabric-"); + found.then_some(issue) +} + +async fn outdated_launcher(log: &str, data: &Data) -> Result { + static OUTDATED_LAUNCHER_REGEX: OnceLock = OnceLock::new(); + let outdated_launcher = OUTDATED_LAUNCHER_REGEX + .get_or_init(|| Regex::new("Prism Launcher version: [0-9].[0-9].[0-9]").unwrap()); + + let Some(captures) = outdated_launcher.captures(log) else { + return Ok(None); + }; + + let octocrab = &data.octocrab; + let version_from_log = captures[0].replace("Prism Launcher version: ", ""); + + let latest_version = if let Some(storage) = &data.storage { + if let Ok(version) = storage.launcher_version().await { + version + } else { + let version = api::github::get_latest_prism_version(octocrab).await?; + storage.cache_launcher_version(&version).await?; + version + } + } else { + trace!("Not caching launcher version, as we're running without a storage backend"); + api::github::get_latest_prism_version(octocrab).await? + }; + + if version_from_log < latest_version { + let issue = ( + "Outdated Prism Launcher".to_string(), + format!("Your installed version is {version_from_log}, while the newest version is {latest_version}.\nPlease update, for more info see https://prismlauncher.org/download/") + ); + + Ok(Some(issue)) + } else { + Ok(None) + } +} + +fn pre_1_12_native_transport_java_9(log: &str) -> Issue { + let issue = ( + "Linux: broken multiplayer with 1.8-1.11 and Java 9+".to_string(), + "These versions of Minecraft use an outdated version of Netty which does not properly support Java 9. + +Switching to Java 8 or below will fix this issue. For more information, type `/tag java`. + +If you must use a newer version, do the following: +- Open `options.txt` (in the main window Edit -> Open .minecraft) and change. +- Find `useNativeTransport:true` and change it to `useNativeTransport:false`. +Note: whilst Netty was introduced in 1.7, this option did not exist \ +which is why the issue was not present." + .to_string(), + ); + + let found = log.contains( + "java.lang.RuntimeException: Unable to access address of buffer\n\tat io.netty.channel.epoll" + ); + + found.then_some(issue) +} + +fn wrong_java(log: &str) -> Issue { + static SWITCH_VERSION_REGEX: OnceLock = OnceLock::new(); + let switch_version = SWITCH_VERSION_REGEX.get_or_init(|| Regex::new( + r"(?m)Please switch to one of the following Java versions for this instance:[\r\n]+(Java version [\d.]+)", +).unwrap()); + + if let Some(captures) = switch_version.captures(log) { + let versions = captures[1].split('\n').collect::>().join(", "); + return Some(( + "Wrong Java Version".to_string(), + format!("Please switch to one of the following: `{versions}`\nFor more information, type `/tag java`"), + )); + } + + let issue = ( + "Java compatibility check skipped".to_string(), + "The Java major version may not work with your Minecraft instance. Please switch to a compatible version".to_string() + ); + + log.contains("Java major version is incompatible. Things might break.") + .then_some(issue) +} diff --git a/src/handlers/event/analyze_logs/mod.rs b/src/handlers/event/analyze_logs/mod.rs new file mode 100644 index 0000000..6b9cf3e --- /dev/null +++ b/src/handlers/event/analyze_logs/mod.rs @@ -0,0 +1,73 @@ +use crate::{consts::Colors, Data}; + +use eyre::Result; +use log::{debug, trace}; +use poise::serenity_prelude::{ + Context, CreateAllowedMentions, CreateEmbed, CreateMessage, Message, +}; + +mod issues; +mod providers; + +use providers::find_log; + +pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> { + trace!( + "Checking message {} from {} for logs", + message.id, + message.author.id + ); + let channel = message.channel_id; + + let log = find_log(&data.http_client, message).await; + + if log.is_err() { + let embed = CreateEmbed::new() + .title("Analysis failed!") + .description("Couldn't download log"); + let allowed_mentions = CreateAllowedMentions::new().replied_user(true); + let our_message = CreateMessage::new() + .reference_message(message) + .allowed_mentions(allowed_mentions) + .embed(embed); + + channel.send_message(ctx, our_message).await?; + + return Ok(()); + } + + let Some(log) = log? else { + debug!("No log found in message! Skipping analysis"); + return Ok(()); + }; + + let issues = issues::find(&log, data).await?; + + let embed = { + let mut e = CreateEmbed::new().title("Log analysis"); + + if issues.is_empty() { + e = e + .color(Colors::Green) + .description("No issues found automatically"); + } else { + e = e.color(Colors::Red); + + for (title, description) in issues { + e = e.field(title, description, false); + } + } + + e + }; + + let allowed_mentions = CreateAllowedMentions::new().replied_user(true); + let message = CreateMessage::new() + .reference_message(message) + .allowed_mentions(allowed_mentions) + .embed(embed); + + channel.send_message(ctx, message).await?; + + Ok(()) +} diff --git a/src/handlers/event/analyze_logs/providers/0x0.rs b/src/handlers/event/analyze_logs/providers/0x0.rs new file mode 100644 index 0000000..300ae0e --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/0x0.rs @@ -0,0 +1,29 @@ +use crate::api::{HttpClient, HttpClientExt}; + +use std::sync::OnceLock; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::Message; +use regex::Regex; + +pub struct _0x0; + +impl super::LogProvider for _0x0 { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: OnceLock = OnceLock::new(); + let regex = REGEX.get_or_init(|| Regex::new(r"https://0x0\.st/\w*.\w*").unwrap()); + + trace!("Checking if message {} is a 0x0 paste", message.id); + regex + .find_iter(&message.content) + .map(|m| m.as_str().to_string()) + .nth(0) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let log = http.get_request(content).await?.text().await?; + + Ok(log) + } +} diff --git a/src/handlers/event/analyze_logs/providers/attachment.rs b/src/handlers/event/analyze_logs/providers/attachment.rs new file mode 100644 index 0000000..4e09c67 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/attachment.rs @@ -0,0 +1,30 @@ +use crate::api::{HttpClient, HttpClientExt}; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::Message; + +pub struct Attachment; + +impl super::LogProvider for Attachment { + async fn find_match(&self, message: &Message) -> Option { + trace!("Checking if message {} has text attachments", message.id); + + message + .attachments + .iter() + .filter_map(|a| { + a.content_type + .as_ref() + .and_then(|ct| ct.starts_with("text/").then_some(a.url.clone())) + }) + .nth(0) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let attachment = http.get_request(content).await?.bytes().await?.to_vec(); + let log = String::from_utf8(attachment)?; + + Ok(log) + } +} diff --git a/src/handlers/event/analyze_logs/providers/haste.rs b/src/handlers/event/analyze_logs/providers/haste.rs new file mode 100644 index 0000000..3ad1c18 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/haste.rs @@ -0,0 +1,31 @@ +use crate::api::{HttpClient, HttpClientExt}; + +use std::sync::OnceLock; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::Message; +use regex::Regex; + +const HASTE: &str = "https://hst.sh"; +const RAW: &str = "/raw"; + +pub struct Haste; + +impl super::LogProvider for Haste { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: OnceLock = OnceLock::new(); + let regex = + REGEX.get_or_init(|| Regex::new(r"https://hst\.sh(?:/raw)?/(\w+(?:\.\w*)?)").unwrap()); + + trace!("Checking if message {} is a hst.sh paste", message.id); + super::get_first_capture(regex, &message.content) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let url = format!("{HASTE}{RAW}/{content}"); + let log = http.get_request(&url).await?.text().await?; + + Ok(log) + } +} diff --git a/src/handlers/event/analyze_logs/providers/mclogs.rs b/src/handlers/event/analyze_logs/providers/mclogs.rs new file mode 100644 index 0000000..e89009a --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/mclogs.rs @@ -0,0 +1,30 @@ +use crate::api::{HttpClient, HttpClientExt}; + +use std::sync::OnceLock; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::Message; +use regex::Regex; + +const MCLOGS: &str = "https://api.mclo.gs/1"; +const RAW: &str = "/raw"; + +pub struct MCLogs; + +impl super::LogProvider for MCLogs { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: OnceLock = OnceLock::new(); + let regex = REGEX.get_or_init(|| Regex::new(r"https://mclo\.gs/(\w+)").unwrap()); + + trace!("Checking if message {} is an mclo.gs paste", message.id); + super::get_first_capture(regex, &message.content) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let url = format!("{MCLOGS}{RAW}/{content}"); + let log = http.get_request(&url).await?.text().await?; + + Ok(log) + } +} diff --git a/src/handlers/event/analyze_logs/providers/mod.rs b/src/handlers/event/analyze_logs/providers/mod.rs new file mode 100644 index 0000000..bb9aa4e --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/mod.rs @@ -0,0 +1,70 @@ +use crate::api::HttpClient; + +use std::slice::Iter; + +use enum_dispatch::enum_dispatch; +use eyre::Result; +use poise::serenity_prelude::Message; +use regex::Regex; + +use self::{ + _0x0::_0x0 as _0x0st, attachment::Attachment, haste::Haste, mclogs::MCLogs, paste_gg::PasteGG, + pastebin::PasteBin, +}; + +#[path = "0x0.rs"] +mod _0x0; +mod attachment; +mod haste; +mod mclogs; +mod paste_gg; +mod pastebin; + +#[enum_dispatch] +pub trait LogProvider { + async fn find_match(&self, message: &Message) -> Option; + async fn fetch(&self, http: &HttpClient, content: &str) -> Result; +} + +fn get_first_capture(regex: &Regex, string: &str) -> Option { + regex + .captures_iter(string) + .find_map(|c| c.get(1).map(|c| c.as_str().to_string())) +} + +#[enum_dispatch(LogProvider)] +enum Provider { + _0x0st, + Attachment, + Haste, + MCLogs, + PasteGG, + PasteBin, +} + +impl Provider { + pub fn iterator() -> Iter<'static, Provider> { + static PROVIDERS: [Provider; 6] = [ + Provider::_0x0st(_0x0st), + Provider::Attachment(Attachment), + Provider::Haste(Haste), + Provider::MCLogs(MCLogs), + Provider::PasteBin(PasteBin), + Provider::PasteGG(PasteGG), + ]; + PROVIDERS.iter() + } +} + +pub async fn find_log(http: &HttpClient, message: &Message) -> Result> { + let providers = Provider::iterator(); + + for provider in providers { + if let Some(found) = provider.find_match(message).await { + let log = provider.fetch(http, &found).await?; + return Ok(Some(log)); + } + } + + Ok(None) +} diff --git a/src/handlers/event/analyze_logs/providers/paste_gg.rs b/src/handlers/event/analyze_logs/providers/paste_gg.rs new file mode 100644 index 0000000..8e69514 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/paste_gg.rs @@ -0,0 +1,37 @@ +use crate::api::{paste_gg, HttpClient}; + +use std::sync::OnceLock; + +use eyre::{OptionExt, Result}; +use log::trace; +use poise::serenity_prelude::Message; +use regex::Regex; + +pub struct PasteGG; + +impl super::LogProvider for PasteGG { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: OnceLock = OnceLock::new(); + let regex = REGEX.get_or_init(|| Regex::new(r"https://paste.gg/p/\w+/(\w+)").unwrap()); + + trace!("Checking if message {} is a paste.gg paste", message.id); + super::get_first_capture(regex, &message.content) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let files = paste_gg::files_from(http, content).await?; + let result = files + .result + .ok_or_eyre("Got an empty result from paste.gg!")?; + + let file_id = result + .iter() + .map(|f| f.id.as_str()) + .nth(0) + .ok_or_eyre("Couldn't get file id from empty paste.gg response!")?; + + let log = paste_gg::get_raw_file(http, content, file_id).await?; + + Ok(log) + } +} diff --git a/src/handlers/event/analyze_logs/providers/pastebin.rs b/src/handlers/event/analyze_logs/providers/pastebin.rs new file mode 100644 index 0000000..4207706 --- /dev/null +++ b/src/handlers/event/analyze_logs/providers/pastebin.rs @@ -0,0 +1,31 @@ +use crate::api::{HttpClient, HttpClientExt}; + +use std::sync::OnceLock; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::Message; +use regex::Regex; + +const PASTEBIN: &str = "https://pastebin.com"; +const RAW: &str = "/raw"; + +pub struct PasteBin; + +impl super::LogProvider for PasteBin { + async fn find_match(&self, message: &Message) -> Option { + static REGEX: OnceLock = OnceLock::new(); + let regex = + REGEX.get_or_init(|| Regex::new(r"https://pastebin\.com(?:/raw)?/(\w+)").unwrap()); + + trace!("Checking if message {} is a pastebin paste", message.id); + super::get_first_capture(regex, &message.content) + } + + async fn fetch(&self, http: &HttpClient, content: &str) -> Result { + let url = format!("{PASTEBIN}{RAW}/{content}"); + let log = http.get_request(&url).await?.text().await?; + + Ok(log) + } +} diff --git a/src/handlers/event/delete_on_reaction.rs b/src/handlers/event/delete_on_reaction.rs new file mode 100644 index 0000000..de3b314 --- /dev/null +++ b/src/handlers/event/delete_on_reaction.rs @@ -0,0 +1,27 @@ +use eyre::{Context as _, Result}; +use log::trace; +use poise::serenity_prelude::{Context, InteractionType, Reaction}; + +pub async fn handle(ctx: &Context, reaction: &Reaction) -> Result<()> { + let user = reaction + .user(ctx) + .await + .wrap_err("Couldn't fetch user from reaction!")?; + + let message = reaction + .message(ctx) + .await + .wrap_err("Couldn't fetch message from reaction!")?; + + if let Some(interaction) = &message.interaction { + if interaction.kind == InteractionType::Command + && interaction.user == user + && reaction.emoji.unicode_eq("❌") + { + trace!("Deleting our own message at the request of {}", user.tag()); + message.delete(ctx).await?; + } + } + + Ok(()) +} diff --git a/src/handlers/event/eta.rs b/src/handlers/event/eta.rs new file mode 100644 index 0000000..02bb24e --- /dev/null +++ b/src/handlers/event/eta.rs @@ -0,0 +1,51 @@ +use std::{sync::OnceLock, time::SystemTime}; + +use eyre::Result; +use log::trace; +use poise::serenity_prelude::{Context, Message}; +use regex::Regex; + +fn regex() -> &'static Regex { + static REGEX: OnceLock = OnceLock::new(); + REGEX.get_or_init(|| Regex::new(r"\beta\b").unwrap()) +} + +const MESSAGES: [&str; 16] = [ + "Sometime", + "Some day", + "Not far", + "The future", + "Never", + "Perhaps tomorrow?", + "There are no ETAs", + "No", + "Nah", + "Yes", + "Yas", + "Next month", + "Next year", + "Next week", + "In Prism Launcher 2.0.0", + "At the appropriate juncture, in due course, in the fullness of time", +]; + +pub async fn handle(ctx: &Context, message: &Message) -> Result<()> { + if !regex().is_match(&message.content) { + trace!( + "The message '{}' (probably) doesn't say ETA", + message.content + ); + return Ok(()); + } + + // no, this isn't actually random. we don't need it to be, though -getchoo + let current_time = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH)? + .as_millis(); + let random_pos = (current_time % MESSAGES.len() as u128) as usize; + + let response = format!("{} <:pofat:1031701005559144458>", MESSAGES[random_pos]); + message.reply(ctx, response).await?; + + Ok(()) +} diff --git a/src/handlers/event/expand_link.rs b/src/handlers/event/expand_link.rs new file mode 100644 index 0000000..b336616 --- /dev/null +++ b/src/handlers/event/expand_link.rs @@ -0,0 +1,20 @@ +use crate::{api::HttpClient, utils}; + +use eyre::Result; +use poise::serenity_prelude::{Context, CreateAllowedMentions, CreateMessage, Message}; + +pub async fn handle(ctx: &Context, http: &HttpClient, message: &Message) -> Result<()> { + let embeds = utils::messages::from_message(ctx, http, message).await?; + + if !embeds.is_empty() { + let allowed_mentions = CreateAllowedMentions::new().replied_user(false); + let reply = CreateMessage::new() + .reference_message(message) + .embeds(embeds) + .allowed_mentions(allowed_mentions); + + message.channel_id.send_message(ctx, reply).await?; + } + + Ok(()) +} diff --git a/src/handlers/event/give_role.rs b/src/handlers/event/give_role.rs new file mode 100644 index 0000000..a063399 --- /dev/null +++ b/src/handlers/event/give_role.rs @@ -0,0 +1,45 @@ +use std::str::FromStr; + +use eyre::Result; +use log::debug; +use poise::serenity_prelude::{ + ComponentInteraction, Context, CreateEmbed, CreateInteractionResponseFollowup, RoleId, +}; + +pub async fn handle(ctx: &Context, component_interaction: &ComponentInteraction) -> Result<()> { + let Some(guild_id) = component_interaction.guild_id else { + debug!("Ignoring component interaction not from guild!"); + return Ok(()); + }; + + let Ok(role_id) = RoleId::from_str(&component_interaction.data.custom_id) else { + debug!("Ignoring component interaction that doesn't contain a role as it's ID"); + return Ok(()); + }; + + component_interaction.defer_ephemeral(ctx).await?; + + let mut followup = CreateInteractionResponseFollowup::new().ephemeral(true); + if let Some(role) = guild_id.roles(ctx).await?.get(&role_id) { + let guild_member = guild_id.member(ctx, component_interaction.user.id).await?; + + let mut embed = CreateEmbed::new(); + if guild_member.roles.contains(&role_id) { + guild_member.remove_role(ctx, role_id).await?; + embed = embed.description(format!("❌ Removed `{}`", role.name)); + } else { + guild_member.add_role(ctx, role_id).await?; + embed = embed.description(format!("✅ Added `{}`", role.name)); + } + + followup = followup.add_embed(embed); + } else { + followup = followup.content(format!( + "Role ID {role_id} doesn't seem to exist. Please let the moderators know!" + )); + } + + component_interaction.create_followup(ctx, followup).await?; + + Ok(()) +} diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs new file mode 100644 index 0000000..528317d --- /dev/null +++ b/src/handlers/event/mod.rs @@ -0,0 +1,88 @@ +use crate::{api, Data, Error}; + +use log::{debug, info, trace}; +use poise::serenity_prelude::{ActivityData, Context, FullEvent, OnlineStatus}; +use poise::FrameworkContext; + +mod analyze_logs; +mod delete_on_reaction; +mod eta; +mod expand_link; +mod give_role; +mod pluralkit; +mod support_onboard; + +pub async fn handle( + ctx: &Context, + event: &FullEvent, + _: FrameworkContext<'_, Data, Error>, + data: &Data, +) -> Result<(), Error> { + match event { + FullEvent::Ready { data_about_bot } => { + info!("Logged in as {}!", data_about_bot.user.name); + + let latest_minecraft_version = + api::prism_meta::latest_minecraft_version(&data.http_client).await?; + let activity = ActivityData::playing(format!("Minecraft {latest_minecraft_version}")); + + info!("Setting presence to activity {activity:#?}"); + ctx.set_presence(Some(activity), OnlineStatus::Online); + } + + FullEvent::InteractionCreate { interaction } => { + if let Some(component_interaction) = interaction.as_message_component() { + give_role::handle(ctx, component_interaction).await?; + } + } + + FullEvent::Message { new_message } => { + trace!("Received message {}", new_message.content); + + // ignore new messages from bots + // note: the webhook_id check allows us to still respond to PK users + if (new_message.author.bot && new_message.webhook_id.is_none()) + || new_message.is_own(ctx) + { + trace!("Ignoring message {} from bot", new_message.id); + return Ok(()); + } + + if let Some(storage) = &data.storage { + let http = &data.http_client; + // detect PK users first to make sure we don't respond to unproxied messages + pluralkit::handle(ctx, http, storage, new_message).await?; + + if storage.is_user_plural(new_message.author.id).await? + && pluralkit::is_message_proxied(http, new_message).await? + { + debug!("Not replying to unproxied PluralKit message"); + return Ok(()); + } + } + + eta::handle(ctx, new_message).await?; + expand_link::handle(ctx, &data.http_client, new_message).await?; + analyze_logs::handle(ctx, new_message, data).await?; + } + + FullEvent::ReactionAdd { add_reaction } => { + trace!( + "Received reaction {} on message {} from {}", + add_reaction.emoji, + add_reaction.message_id.to_string(), + add_reaction.user_id.unwrap_or_default().to_string() + ); + delete_on_reaction::handle(ctx, add_reaction).await?; + } + + FullEvent::ThreadCreate { thread } => { + trace!("Received thread {}", thread.id); + support_onboard::handle(ctx, thread).await?; + } + + _ => {} + } + + Ok(()) +} diff --git a/src/handlers/event/pluralkit.rs b/src/handlers/event/pluralkit.rs new file mode 100644 index 0000000..a53434c --- /dev/null +++ b/src/handlers/event/pluralkit.rs @@ -0,0 +1,53 @@ +use crate::{ + api::{self, HttpClient}, + storage::Storage, +}; + +use std::time::Duration; + +use eyre::Result; +use log::{debug, trace}; +use poise::serenity_prelude::{Context, Message}; +use tokio::time::sleep; + +const PK_DELAY: Duration = Duration::from_secs(1); + +pub async fn is_message_proxied(http: &HttpClient, message: &Message) -> Result { + trace!( + "Waiting on PluralKit API for {} seconds", + PK_DELAY.as_secs() + ); + sleep(PK_DELAY).await; + + let proxied = api::pluralkit::sender_from(http, message.id).await.is_ok(); + + Ok(proxied) +} + +pub async fn handle( + _: &Context, + http: &HttpClient, + storage: &Storage, + msg: &Message, +) -> Result<()> { + if msg.webhook_id.is_none() { + return Ok(()); + } + + debug!( + "Message {} has a webhook ID. Checking if it was sent through PluralKit", + msg.id + ); + + trace!( + "Waiting on PluralKit API for {} seconds", + PK_DELAY.as_secs() + ); + sleep(PK_DELAY).await; + + if let Ok(sender) = api::pluralkit::sender_from(http, msg.id).await { + storage.store_user_plurality(sender).await?; + } + + Ok(()) +} diff --git a/src/handlers/event/support_onboard.rs b/src/handlers/event/support_onboard.rs new file mode 100644 index 0000000..f2d2d5f --- /dev/null +++ b/src/handlers/event/support_onboard.rs @@ -0,0 +1,52 @@ +use eyre::{eyre, OptionExt, Result}; +use log::{debug, trace}; +use poise::serenity_prelude::{ + ChannelType, Context, CreateAllowedMentions, CreateMessage, GuildChannel, +}; + +pub async fn handle(ctx: &Context, thread: &GuildChannel) -> Result<()> { + if thread.kind != ChannelType::PublicThread { + trace!("Not doing support onboard in non-public thread channel"); + return Ok(()); + } + + if thread.last_message_id.is_some() { + debug!("Ignoring duplicate thread creation event"); + return Ok(()); + } + + if thread + .parent_id + .ok_or_else(|| eyre!("Couldn't get parent ID from thread {}!", thread.name))? + .name(ctx) + .await + .unwrap_or_default() + != "support" + { + debug!("Not posting onboarding message to threads outside of support"); + return Ok(()); + } + + let owner = thread + .owner_id + .ok_or_eyre("Couldn't get owner of thread!")?; + + let msg = format!( + "<@{}> We've received your support ticket! {} {}", + owner, + "Please upload your logs and post the link here if possible (run `tag log` to find out how).", + "Please don't ping people for support questions, unless you have their permission." + ); + + let allowed_mentions = CreateAllowedMentions::new() + .replied_user(true) + .users(Vec::from([owner])); + + let message = CreateMessage::new() + .content(msg) + .allowed_mentions(allowed_mentions); + + thread.send_message(ctx, message).await?; + + Ok(()) +} diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs new file mode 100644 index 0000000..2ae0539 --- /dev/null +++ b/src/handlers/mod.rs @@ -0,0 +1,5 @@ +mod error; +mod event; + +pub use error::handle as handle_error; +pub use event::handle as handle_event; diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index f3f344b..0000000 --- a/src/index.ts +++ /dev/null @@ -1,223 +0,0 @@ -import { - Client, - GatewayIntentBits, - Partials, - OAuth2Scopes, - InteractionType, - PermissionFlagsBits, - ChannelType, - Events, -} from 'discord.js'; -import { reuploadCommands } from './_reupload'; -import { - connect as connectStorage, - isUserPlural, - storeUserPlurality, -} from './storage'; - -import * as BuildConfig from './constants'; -import { parseLog } from './logs'; -import { getLatestMinecraftVersion } from './utils/remoteVersions'; -import { expandDiscordLink } from './utils/resolveMessage'; - -import { membersCommand } from './commands/members'; -import { starsCommand } from './commands/stars'; -import { modrinthCommand } from './commands/modrinth'; -import { tagsCommand } from './commands/tags'; -import { jokeCommand } from './commands/joke'; -import { roryCommand } from './commands/rory'; -import { sayCommand } from './commands/say'; - -import random from 'just-random'; -import { green, bold, yellow, cyan } from 'kleur/colors'; -import 'dotenv/config'; -import { - fetchPluralKitMessage, - isMessageProxied, - pkDelay, -} from './utils/pluralKit'; - -const client = new Client({ - intents: [ - GatewayIntentBits.Guilds, - GatewayIntentBits.GuildMessages, - GatewayIntentBits.MessageContent, - GatewayIntentBits.DirectMessages, - GatewayIntentBits.GuildMembers, - GatewayIntentBits.GuildPresences, - GatewayIntentBits.GuildMessageReactions, - GatewayIntentBits.GuildModeration, - ], - partials: [Partials.Channel], -}); - -client.once('ready', async () => { - console.log(green('Discord bot ready!')); - - console.log( - cyan( - client.generateInvite({ - scopes: [OAuth2Scopes.Bot], - permissions: [ - PermissionFlagsBits.AddReactions, - PermissionFlagsBits.ViewChannel, - PermissionFlagsBits.BanMembers, - PermissionFlagsBits.KickMembers, - PermissionFlagsBits.CreatePublicThreads, - PermissionFlagsBits.CreatePrivateThreads, - PermissionFlagsBits.EmbedLinks, - PermissionFlagsBits.ManageChannels, - PermissionFlagsBits.ManageRoles, - PermissionFlagsBits.ModerateMembers, - PermissionFlagsBits.MentionEveryone, - PermissionFlagsBits.MuteMembers, - PermissionFlagsBits.SendMessages, - PermissionFlagsBits.SendMessagesInThreads, - PermissionFlagsBits.ReadMessageHistory, - ], - }) - ) - ); - - if (process.env.NODE_ENV !== 'development') - console.warn(yellow(bold('Running in production mode!'))); - - const mcVersion = await getLatestMinecraftVersion(); - client.user?.presence.set({ - activities: [{ name: `Minecraft ${mcVersion}` }], - status: 'online', - }); - - client.on(Events.MessageCreate, async (e) => { - try { - if (e.channel.partial) await e.channel.fetch(); - if (e.author.partial) await e.author.fetch(); - - if (!e.content) return; - if (!e.channel.isTextBased()) return; - - if (e.author === client.user) return; - - if (e.webhookId !== null) { - // defer PK detection - setTimeout(async () => { - const pkMessage = await fetchPluralKitMessage(e); - if (pkMessage !== null) storeUserPlurality(pkMessage.sender); - }, pkDelay); - } - - if ((await isUserPlural(e.author.id)) && (await isMessageProxied(e))) - return; - - if (e.cleanContent.match(BuildConfig.ETA_REGEX)) { - await e.reply( - `${random(BuildConfig.ETA_MESSAGES)} <:pofat:1031701005559144458>` - ); - } - const log = await parseLog(e.content); - if (log != null) { - e.reply({ embeds: [log] }); - return; - } - await expandDiscordLink(e); - } catch (error) { - console.error('Unhandled exception on MessageCreate', error); - } - }); -}); - -client.on(Events.InteractionCreate, async (interaction) => { - try { - if (!interaction.isChatInputCommand()) return; - - const { commandName } = interaction; - - if (commandName === 'ping') { - await interaction.reply({ - content: `Pong! \`${client.ws.ping}ms\``, - ephemeral: true, - }); - } else if (commandName === 'members') { - await membersCommand(interaction); - } else if (commandName === 'stars') { - await starsCommand(interaction); - } else if (commandName === 'modrinth') { - await modrinthCommand(interaction); - } else if (commandName === 'say') { - await sayCommand(interaction); - } else if (commandName === 'tag') { - await tagsCommand(interaction); - } else if (commandName === 'joke') { - await jokeCommand(interaction); - } else if (commandName === 'rory') { - await roryCommand(interaction); - } - } catch (error) { - console.error('Unhandled exception on InteractionCreate', error); - } -}); - -client.on(Events.MessageReactionAdd, async (reaction, user) => { - try { - if (reaction.partial) { - try { - await reaction.fetch(); - } catch (error) { - console.error('Something went wrong when fetching the message:', error); - return; - } - } - - if ( - reaction.message.interaction && - reaction.message.interaction?.type === - InteractionType.ApplicationCommand && - reaction.message.interaction?.user === user && - reaction.emoji.name === '❌' - ) { - await reaction.message.delete(); - } - } catch (error) { - console.error('Unhandled exception on MessageReactionAdd', error); - } -}); - -client.on(Events.ThreadCreate, async (channel) => { - try { - if ( - channel.type === ChannelType.PublicThread && - channel.parent && - channel.parent.name === 'support' && - channel.guild - ) { - const pingRole = channel.guild.roles.cache.find( - (r) => r.name === 'Moderators' - ); - - if (!pingRole) return; - - await channel.send({ - content: ` - <@${channel.ownerId}> We've received your support ticket! Please upload your logs and post the link here if possible (run \`/tag log\` to find out how). Please don't ping people for support questions, unless you have their permission. - `.trim(), - allowedMentions: { - repliedUser: true, - roles: [], - users: channel.ownerId ? [channel.ownerId] : [], - }, - }); - } - } catch (error) { - console.error('Error handling ThreadCreate', error); - } -}); - -reuploadCommands() - .then(() => { - connectStorage(); - client.login(process.env.DISCORD_TOKEN); - }) - .catch((e) => { - console.error(e); - process.exit(1); - }); diff --git a/src/logproviders/0x0.ts b/src/logproviders/0x0.ts deleted file mode 100644 index 65774e6..0000000 --- a/src/logproviders/0x0.ts +++ /dev/null @@ -1,19 +0,0 @@ -const reg = /https:\/\/0x0.st\/\w*.\w*/; - -export async function read0x0(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - let log: string; - try { - const f = await fetch(link); - if (f.status != 200) { - throw 'nope'; - } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} diff --git a/src/logproviders/haste.ts b/src/logproviders/haste.ts deleted file mode 100644 index c295f41..0000000 --- a/src/logproviders/haste.ts +++ /dev/null @@ -1,21 +0,0 @@ -const reg = /https:\/\/hst.sh\/[\w]*/; - -export async function readHastebin(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace('https://hst.sh/', ''); - if (!id) return null; - let log: string; - try { - const f = await fetch(`https://hst.sh/raw/${id}`); - if (f.status != 200) { - throw 'nope'; - } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} diff --git a/src/logproviders/mclogs.ts b/src/logproviders/mclogs.ts deleted file mode 100644 index eecda09..0000000 --- a/src/logproviders/mclogs.ts +++ /dev/null @@ -1,22 +0,0 @@ -const reg = /https:\/\/mclo.gs\/\w*/; - -export async function readMcLogs(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace('https://mclo.gs/', ''); - if (!id) return null; - const apiUrl = 'https://api.mclo.gs/1/raw/' + id; - let log: string; - try { - const f = await fetch(apiUrl); - if (f.status != 200) { - throw 'nope'; - } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} diff --git a/src/logproviders/pastegg.ts b/src/logproviders/pastegg.ts deleted file mode 100644 index 11181e7..0000000 --- a/src/logproviders/pastegg.ts +++ /dev/null @@ -1,30 +0,0 @@ -const reg = /https:\/\/paste.gg\/p\/[\w]*\/[\w]*/; - -export async function readPasteGG(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace(/https:\/\/paste.gg\/p\/[\w]*\//, ''); - if (!id) return null; - let log: string; - try { - const pasteJson = await ( - await fetch('https://api.paste.gg/v1/pastes/' + id) - ).json(); - if (pasteJson.status != 'success') throw 'up'; - const pasteData = await ( - await fetch( - 'https://api.paste.gg/v1/pastes/' + - id + - '/files/' + - pasteJson.result.files[0].id - ) - ).json(); - if (pasteData.status != 'success') throw 'up'; - return pasteData.result.content.value; - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} diff --git a/src/logs.ts b/src/logs.ts deleted file mode 100644 index 08c5c39..0000000 --- a/src/logs.ts +++ /dev/null @@ -1,251 +0,0 @@ -import { getLatestPrismLauncherVersion } from './utils/remoteVersions'; -import { EmbedBuilder } from 'discord.js'; - -// log providers -import { readMcLogs } from './logproviders/mclogs'; -import { read0x0 } from './logproviders/0x0'; -import { readPasteGG } from './logproviders/pastegg'; -import { readHastebin } from './logproviders/haste'; -import { COLORS } from './constants'; - -type Analyzer = (text: string) => Promise | null>; -type LogProvider = (text: string) => Promise; - -const javaAnalyzer: Analyzer = async (text) => { - if (text.includes('This instance is not compatible with Java version')) { - const xp = - /Please switch to one of the following Java versions for this instance:[\r\n]+(Java version (?:\d|\.)+)/g; - - let ver: string; - const m = text.match(xp); - if (!m || !m[0]) { - ver = ''; - } else { - ver = m[0].split('\n')[1]; - } - - return [ - 'Wrong Java Version', - `Please switch to the following: \`${ver}\`\nFor more information, type \`/tag java\``, - ]; - } else if ( - text.includes('Java major version is incompatible. Things might break.') - ) { - return [ - 'Java compatibility check skipped', - 'The Java major version may not work with your Minecraft instance. Please switch to a compatible version', - ]; - } - return null; -}; - -const versionAnalyzer: Analyzer = async (text) => { - const vers = text.match(/Prism Launcher version: [0-9].[0-9].[0-9]/g); - if (vers && vers[0]) { - const latest = await getLatestPrismLauncherVersion(); - const current = vers[0].replace('Prism Launcher version: ', ''); - if (current < latest) { - return [ - 'Outdated Prism Launcher', - `Your installed version is ${current}, while the newest version is ${latest}.\nPlease update, for more info see https://prismlauncher.org/download/`, - ]; - } - } - return null; -}; - -const flatpakNvidiaAnalyzer: Analyzer = async (text) => { - if ( - text.includes('org.lwjgl.LWJGLException: Could not choose GLX13 config') || - text.includes( - 'GLFW error 65545: GLX: Failed to find a suitable GLXFBConfig' - ) - ) { - return [ - 'Outdated Nvidia Flatpak Driver', - `The Nvidia driver for flatpak is outdated.\nPlease run \`flatpak update\` to fix this issue. If that does not solve it, please wait until the driver is added to Flathub and run it again.`, - ]; - } - return null; -}; - -const forgeJavaAnalyzer: Analyzer = async (text) => { - if ( - text.includes( - 'java.lang.NoSuchMethodError: sun.security.util.ManifestEntryVerifier.(Ljava/util/jar/Manifest;)V' - ) - ) { - return [ - 'Forge Java Bug', - 'Old versions of Forge crash with Java 8u321+.\nTo fix this, update forge to the latest version via the Versions tab \n(right click on Forge, click Change Version, and choose the latest one)\nAlternatively, you can download 8u312 or lower. See [archive](https://github.com/adoptium/temurin8-binaries/releases/tag/jdk8u312-b07)', - ]; - } - return null; -}; - -const intelHDAnalyzer: Analyzer = async (text) => { - if (text.includes('org.lwjgl.LWJGLException: Pixel format not accelerated')) { - return [ - 'Intel HD Windows 10', - "Your drivers don't support windows 10 officially\nSee https://prismlauncher.org/wiki/getting-started/installing-java/#a-note-about-intel-hd-20003000-on-windows-10 for more info", - ]; - } - return null; -}; - -const macOSNSWindowAnalyzer: Analyzer = async (text) => { - if ( - text.includes( - "Terminating app due to uncaught exception 'NSInternalInconsistencyException'" - ) - ) { - return [ - 'MacOS NSInternalInconsistencyException', - 'You need to downgrade your Java 8 version. See https://prismlauncher.org/wiki/getting-started/installing-java/#older-minecraft-on-macos', - ]; - } - return null; -}; - -const quiltFabricInternalsAnalyzer: Analyzer = async (text) => { - const base = 'Caused by: java.lang.ClassNotFoundException: '; - if ( - text.includes(base + 'net.fabricmc.fabric.impl') || - text.includes(base + 'net.fabricmc.fabric.mixin') || - text.includes(base + 'net.fabricmc.loader.impl') || - text.includes(base + 'net.fabricmc.loader.mixin') || - text.includes( - 'org.quiltmc.loader.impl.FormattedException: java.lang.NoSuchMethodError:' - ) - ) { - return [ - 'Fabric Internal Access', - `The mod you are using is using fabric internals that are not meant to be used by anything but the loader itself. - Those mods break both on Quilt and with fabric updates. - If you're using fabric, downgrade your fabric loader could work, on Quilt you can try updating to the latest beta version, but there's nothing much to do unless the mod author stops using them.`, - ]; - } - return null; -}; - -const oomAnalyzer: Analyzer = async (text) => { - if (text.includes('java.lang.OutOfMemoryError')) { - return [ - 'Out of Memory', - 'Allocating more RAM to your instance could help prevent this crash.', - ]; - } - return null; -}; - -const javaOptionsAnalyzer: Analyzer = async (text) => { - const r1 = /Unrecognized VM option '(\w*)'/; - const r2 = /Unrecognized option: \w*/; - const m1 = text.match(r1); - const m2 = text.match(r2); - if (m1) { - return [ - 'Wrong Java Arguments', - `Remove \`-XX:${m1[1]}\` from your Java arguments.`, - ]; - } - if (m2) { - return [ - 'Wrong Java Arguments', - `Remove \`${m2[1]}\` from your Java arguments.`, - ]; - } - return null; -}; - -const shenadoahGCAnalyzer: Analyzer = async (text) => { - if (text.includes("Unrecognized VM option 'UseShenandoahGC'")) { - return [ - "Java 8 doesn't support ShenandoahGC", - 'Remove `UseShenandoahGC` from your Java Arguments', - ]; - } - return null; -}; - -const optifineAnalyzer: Analyzer = async (text) => { - const matchesOpti = text.match(/\[✔️\] OptiFine_[\w,.]*/); - const matchesOptiFabric = text.match(/\[✔️\] optifabric-[\w,.]*/); - if (matchesOpti || matchesOptiFabric) { - return [ - 'Possible Optifine Problems', - 'OptiFine is known to cause problems when paired with other mods. Try to disable OptiFine and see if the issue persists.\nCheck `/tag optifine` for more info & alternatives you can use.', - ]; - } - return null; -}; - -const analyzers: Analyzer[] = [ - javaAnalyzer, - versionAnalyzer, - flatpakNvidiaAnalyzer, - forgeJavaAnalyzer, - intelHDAnalyzer, - macOSNSWindowAnalyzer, - quiltFabricInternalsAnalyzer, - oomAnalyzer, - shenadoahGCAnalyzer, - optifineAnalyzer, - javaOptionsAnalyzer, -]; - -const providers: LogProvider[] = [ - readMcLogs, - read0x0, - readPasteGG, - readHastebin, -]; - -export async function parseLog(s: string): Promise { - if (/(https?:\/\/)?pastebin\.com\/(raw\/)?[^/\s]{8}/g.test(s)) { - const embed = new EmbedBuilder() - .setTitle('pastebin.com detected') - .setDescription( - 'Please use https://mclo.gs or another paste provider and send logs using the Log Upload feature in Prism Launcher. (See `/tag log`)' - ) - .setColor(COLORS.red); - return embed; - } - - let log = ''; - for (const i in providers) { - const provider = providers[i]; - const res = await provider(s); - if (res) { - log = res; - break; - } else { - continue; - } - } - if (!log) return null; - const embed = new EmbedBuilder().setTitle('Log analysis'); - - let thereWasAnIssue = false; - for (const i in analyzers) { - const Analyzer = analyzers[i]; - const out = await Analyzer(log); - if (out) { - embed.addFields({ name: out[0], value: out[1] }); - thereWasAnIssue = true; - } - } - - if (thereWasAnIssue) { - embed.setColor(COLORS.red); - return embed; - } else { - embed.setColor(COLORS.green); - embed.addFields({ - name: 'Analyze failed', - value: 'No issues found automatically', - }); - - return embed; - } -} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0d6f1b1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,145 @@ +use std::{sync::Arc, time::Duration}; + +use eyre::Context as _; +use log::{info, trace, warn}; +use poise::{ + serenity_prelude as serenity, EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, +}; +use tokio::signal::ctrl_c; +#[cfg(target_family = "unix")] +use tokio::signal::unix::{signal, SignalKind}; +#[cfg(target_family = "windows")] +use tokio::signal::windows::ctrl_close; + +mod api; +mod commands; +mod config; +mod consts; +mod handlers; +mod storage; +mod tags; +mod utils; + +use config::Config; +use storage::Storage; + +type Error = Box; +type Context<'a> = poise::Context<'a, Data, Error>; + +#[derive(Clone, Debug, Default)] +pub struct Data { + config: Config, + storage: Option, + http_client: api::HttpClient, + octocrab: Arc, +} + +async fn setup( + ctx: &serenity::Context, + _: &serenity::Ready, + framework: &Framework, +) -> Result { + let config = Config::new_from_env(); + + let storage = if let Some(url) = &config.bot.redis_url { + Some(Storage::from_url(url)?) + } else { + None + }; + + if let Some(storage) = storage.as_ref() { + if !storage.clone().has_connection() { + return Err( + "You specified a storage backend but there's no connection! Is it running?".into(), + ); + } + + trace!("Redis connection looks good!"); + } + + let http_client = api::HttpClient::default(); + let octocrab = octocrab::instance(); + + let data = Data { + config, + storage, + http_client, + octocrab, + }; + + poise::builtins::register_globally(ctx, &framework.options().commands).await?; + info!("Registered global commands!"); + + Ok(data) +} + +async fn handle_shutdown(shard_manager: Arc, reason: &str) { + warn!("{reason}! Shutting down bot..."); + shard_manager.shutdown_all().await; + println!("Everything is shutdown. Goodbye!"); +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + dotenvy::dotenv().ok(); + color_eyre::install()?; + env_logger::init(); + + let token = + std::env::var("DISCORD_BOT_TOKEN").wrap_err("Couldn't find bot token in environment!")?; + + let intents = + serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; + + let options = FrameworkOptions { + commands: commands::all(), + + on_error: |error| Box::pin(handlers::handle_error(error)), + + command_check: Some(|ctx| { + Box::pin(async move { Ok(ctx.author().id != ctx.framework().bot_id) }) + }), + + event_handler: |ctx, event, framework, data| { + Box::pin(handlers::handle_event(ctx, event, framework, data)) + }, + + prefix_options: PrefixFrameworkOptions { + prefix: Some(".".into()), + edit_tracker: Some(Arc::from(EditTracker::for_timespan(Duration::from_secs( + 3600, + )))), + ..Default::default() + }, + + ..Default::default() + }; + + let framework = Framework::builder() + .options(options) + .setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework))) + .build(); + + let mut client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await?; + + let shard_manager = client.shard_manager.clone(); + + #[cfg(target_family = "unix")] + let mut sigterm = signal(SignalKind::terminate())?; + #[cfg(target_family = "windows")] + let mut sigterm = ctrl_close()?; + + tokio::select! { + result = client.start() => result.map_err(eyre::Report::from), + _ = sigterm.recv() => { + handle_shutdown(shard_manager, "Received SIGTERM").await; + std::process::exit(0); + } + _ = ctrl_c() => { + handle_shutdown(shard_manager, "Interrupted").await; + std::process::exit(130); + } + } +} diff --git a/src/storage.ts b/src/storage.ts deleted file mode 100644 index c3b40c9..0000000 --- a/src/storage.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { createClient } from 'redis'; - -export const client = createClient({ - url: process.env.REDIS_URL || 'redis://localhost:6379', -}); - -export const storeUserPlurality = async (userId: string) => { - // Just store some value. We only care about the presence of this key - await client - .multi() - .set(`user:${userId}:pk`, '0') - .expire(`user:${userId}:pk`, 7 * 24 * 60 * 60) - .exec(); -}; - -export const isUserPlural = async (userId: string) => { - return (await client.exists(`user:${userId}:pk`)) > 0; -}; - -export const connect = () => { - client.connect(); -}; diff --git a/src/storage/mod.rs b/src/storage/mod.rs new file mode 100644 index 0000000..e482b4f --- /dev/null +++ b/src/storage/mod.rs @@ -0,0 +1,90 @@ +use std::fmt::Debug; + +use eyre::Result; +use log::debug; +use poise::serenity_prelude::UserId; +use redis::{AsyncCommands, Client, ConnectionLike}; + +const PK_KEY: &str = "pluralkit-v1"; +const LAUNCHER_VERSION_KEY: &str = "launcher-version-v1"; +const LAUNCHER_STARGAZER_KEY: &str = "launcher-stargazer-v1"; + +#[derive(Clone, Debug)] +pub struct Storage { + client: Client, +} + +impl Storage { + pub fn new(client: Client) -> Self { + Self { client } + } + + pub fn from_url(redis_url: &str) -> Result { + let client = Client::open(redis_url)?; + + Ok(Self::new(client)) + } + + pub fn has_connection(&mut self) -> bool { + self.client.check_connection() + } + + pub async fn store_user_plurality(&self, sender: UserId) -> Result<()> { + debug!("Marking {sender} as a PluralKit user"); + let key = format!("{PK_KEY}:{sender}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + // Just store some value. We only care about the presence of this key + con.set_ex(key, 0, 7 * 24 * 60 * 60).await?; // 1 week + + Ok(()) + } + + pub async fn is_user_plural(&self, user_id: UserId) -> Result { + debug!("Checking if user {user_id} is plural"); + let key = format!("{PK_KEY}:{user_id}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + let exists = con.exists(key).await?; + + Ok(exists) + } + + pub async fn cache_launcher_version(&self, version: &str) -> Result<()> { + debug!("Caching launcher version as {version}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + con.set_ex(LAUNCHER_VERSION_KEY, version, 24 * 60 * 60) + .await?; // 1 day + + Ok(()) + } + + pub async fn launcher_version(&self) -> Result { + debug!("Fetching launcher version"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + let res = con.get(LAUNCHER_VERSION_KEY).await?; + + Ok(res) + } + + pub async fn cache_launcher_stargazer_count(&self, stargazers: u32) -> Result<()> { + debug!("Caching stargazer count as {stargazers}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + con.set_ex(LAUNCHER_STARGAZER_KEY, stargazers, 60 * 60) + .await?; + + Ok(()) + } + + pub async fn launcher_stargazer_count(&self) -> Result { + debug!("Fetching launcher stargazer count"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + let res: u32 = con.get(LAUNCHER_STARGAZER_KEY).await?; + + Ok(res) + } +} diff --git a/src/tags.rs b/src/tags.rs new file mode 100644 index 0000000..2d6336d --- /dev/null +++ b/src/tags.rs @@ -0,0 +1,20 @@ +use poise::serenity_prelude::EmbedField; +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] +pub const TAG_DIR: &str = "tags"; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct TagFrontmatter { + pub title: String, + pub color: Option, + pub image: Option, + pub fields: Option>, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Tag { + pub content: String, + pub id: String, + pub frontmatter: TagFrontmatter, +} diff --git a/src/tags.ts b/src/tags.ts deleted file mode 100644 index 3df1e2e..0000000 --- a/src/tags.ts +++ /dev/null @@ -1,37 +0,0 @@ -import matter from 'gray-matter'; -import { readdir, readFile } from 'fs/promises'; -import { join } from 'path'; -import { COLORS } from './constants'; - -import { type EmbedField } from 'discord.js'; - -interface Tag { - name: string; - aliases?: string[]; - title?: string; - color?: number; - content: string; - image?: string; - fields?: EmbedField[]; -} - -const TAG_DIR = join(process.cwd(), 'tags'); - -export const getTags = async (): Promise => { - const filenames = await readdir(TAG_DIR); - const tags: Tag[] = []; - - for (const _file of filenames) { - const file = join(TAG_DIR, _file); - const { data, content } = matter(await readFile(file)); - - tags.push({ - ...data, - name: _file.replace('.md', ''), - content: content.trim(), - color: data.color ? COLORS[data.color] : undefined, - }); - } - - return tags; -}; diff --git a/src/utils/messages.rs b/src/utils/messages.rs new file mode 100644 index 0000000..830e49f --- /dev/null +++ b/src/utils/messages.rs @@ -0,0 +1,175 @@ +use crate::api::{pluralkit, HttpClient}; + +use std::{str::FromStr, sync::OnceLock}; + +use eyre::{eyre, Context as _, Result}; +use log::{debug, trace}; +use poise::serenity_prelude::{ + Cache, CacheHttp, ChannelId, ChannelType, Colour, Context, CreateEmbed, CreateEmbedAuthor, + CreateEmbedFooter, GuildChannel, Member, Message, MessageId, Permissions, UserId, +}; +use regex::Regex; + +fn find_first_image(message: &Message) -> Option { + message + .attachments + .iter() + .find(|a| { + a.content_type + .as_ref() + .unwrap_or(&String::new()) + .starts_with("image/") + }) + .map(|res| res.url.clone()) +} + +async fn find_real_author_id(http: &HttpClient, message: &Message) -> UserId { + if let Ok(sender) = pluralkit::sender_from(http, message.id).await { + sender + } else { + message.author.id + } +} + +async fn member_can_view_channel( + ctx: impl CacheHttp + AsRef, + member: &Member, + channel: &GuildChannel, +) -> Result { + static REQUIRED_PERMISSIONS: OnceLock = OnceLock::new(); + let required_permissions = REQUIRED_PERMISSIONS + .get_or_init(|| Permissions::VIEW_CHANNEL | Permissions::READ_MESSAGE_HISTORY); + + let guild = ctx.http().get_guild(channel.guild_id).await?; + + let channel_to_check = match &channel.kind { + ChannelType::PublicThread => { + let parent_id = channel + .parent_id + .ok_or_else(|| eyre!("Couldn't get parent of thread {}", channel.id))?; + parent_id + .to_channel(ctx) + .await? + .guild() + .ok_or_else(|| eyre!("Couldn't get GuildChannel from ChannelID {parent_id}!"))? + } + + ChannelType::Text | ChannelType::News => channel.to_owned(), + + _ => return Ok(false), + }; + + let can_view = guild + .user_permissions_in(&channel_to_check, member) + .contains(*required_permissions); + Ok(can_view) +} + +pub async fn to_embed( + ctx: impl CacheHttp + AsRef, + message: &Message, +) -> Result { + let author = CreateEmbedAuthor::new(message.author.tag()).icon_url( + message + .author + .avatar_url() + .unwrap_or_else(|| message.author.default_avatar_url()), + ); + + let footer = CreateEmbedFooter::new(format!( + "#{}", + message.channel(ctx).await?.guild().unwrap_or_default().name + )); + + let mut embed = CreateEmbed::new() + .author(author) + .color(Colour::BLITZ_BLUE) + .timestamp(message.timestamp) + .footer(footer) + .description(format!( + "{}\n\n[Jump to original message]({})", + message.content, + message.link() + )); + + if !message.attachments.is_empty() { + embed = embed.fields(message.attachments.iter().map(|a| { + ( + "Attachments".to_string(), + format!("[{}]({})", a.filename, a.url), + false, + ) + })); + + if let Some(image) = find_first_image(message) { + embed = embed.image(image); + } + } + + Ok(embed) +} + +pub async fn from_message( + ctx: &Context, + http: &HttpClient, + msg: &Message, +) -> Result> { + static MESSAGE_PATTERN: OnceLock = OnceLock::new(); + let message_pattern = MESSAGE_PATTERN.get_or_init(|| Regex::new(r"(?:https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?\d+)\/(?\d+)\/(?\d+)").unwrap()); + + let Some(guild_id) = msg.guild_id else { + debug!("Not resolving message in DM"); + return Ok(Vec::new()); + }; + + // if the message was sent through pluralkit, we'll want + // to reference the Member of the unproxied account + let author_id = if msg.webhook_id.is_some() { + find_real_author_id(http, msg).await + } else { + msg.author.id + }; + + let author = guild_id.member(ctx, author_id).await?; + + let matches = message_pattern + .captures_iter(&msg.content) + .map(|capture| capture.extract()); + + let mut embeds: Vec = vec![]; + + for (url, [target_guild_id, target_channel_id, target_message_id]) in matches { + if target_guild_id != guild_id.to_string() { + debug!("Not resolving message from other server"); + continue; + } + trace!("Attempting to resolve message {target_message_id} from URL {url}"); + + let target_channel = ChannelId::from_str(target_channel_id)? + .to_channel(ctx) + .await? + .guild() + .ok_or_else(|| { + eyre!("Couldn't find GuildChannel from ChannelId {target_channel_id}!") + })?; + + if !member_can_view_channel(ctx, &author, &target_channel).await? { + debug!("Not resolving message for author who can't see it"); + continue; + } + + let target_message_id = MessageId::from_str(target_message_id)?; + let target_message = target_channel + .message(ctx, target_message_id) + .await + .wrap_err_with(|| { + eyre!("Couldn't find channel message from ID {target_message_id}!") + })?; + + let embed = to_embed(ctx, &target_message).await?; + + embeds.push(embed); + } + + Ok(embeds) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..8d9dd97 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,10 @@ +use poise::serenity_prelude::{CreateEmbedAuthor, User}; + +pub mod messages; + +pub fn embed_author_from_user(user: &User) -> CreateEmbedAuthor { + CreateEmbedAuthor::new(user.tag()).icon_url( + user.avatar_url() + .unwrap_or_else(|| user.default_avatar_url()), + ) +} diff --git a/src/utils/pluralKit.ts b/src/utils/pluralKit.ts deleted file mode 100644 index 3f5aae3..0000000 --- a/src/utils/pluralKit.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Message } from 'discord.js'; - -interface PkMessage { - sender: string; -} - -export const pkDelay = 1000; - -export async function fetchPluralKitMessage(message: Message) { - const response = await fetch( - `https://api.pluralkit.me/v2/messages/${message.id}` - ); - - if (!response.ok) return null; - - return (await response.json()) as PkMessage; -} - -export async function isMessageProxied(message: Message) { - await new Promise((resolve) => setTimeout(resolve, pkDelay)); - return (await fetchPluralKitMessage(message)) !== null; -} diff --git a/src/utils/remoteVersions.ts b/src/utils/remoteVersions.ts deleted file mode 100644 index d862787..0000000 --- a/src/utils/remoteVersions.ts +++ /dev/null @@ -1,30 +0,0 @@ -interface MetaPackage { - formatVersion: number; - name: string; - recommended: string[]; - uid: string; -} - -interface SimplifiedGHReleases { - tag_name: string; -} - -// TODO: caching -export async function getLatestMinecraftVersion(): Promise { - const f = await fetch( - 'https://meta.prismlauncher.org/v1/net.minecraft/package.json' - ); - - const minecraft = (await f.json()) as MetaPackage; - return minecraft.recommended[0]; -} - -// TODO: caching -export async function getLatestPrismLauncherVersion(): Promise { - const f = await fetch( - 'https://api.github.com/repos/PrismLauncher/PrismLauncher/releases' - ); - const versions = (await f.json()) as SimplifiedGHReleases[]; - - return versions[0].tag_name; -} diff --git a/src/utils/resolveMessage.ts b/src/utils/resolveMessage.ts deleted file mode 100644 index 8b7c0d2..0000000 --- a/src/utils/resolveMessage.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { - Colors, - EmbedBuilder, - type Message, - ThreadChannel, - ReactionCollector, -} from 'discord.js'; - -function findFirstImage(message: Message): string | undefined { - const result = message.attachments.find((attach) => { - return attach.contentType?.startsWith('image/'); - }); - - if (result === undefined) { - return undefined; - } else { - return result.url; - } -} - -export async function expandDiscordLink(message: Message): Promise { - if (message.author.bot && !message.webhookId) return; - - const re = - /(https?:\/\/)?(?:canary\.|ptb\.)?discord(?:app)?\.com\/channels\/(?\d+)\/(?\d+)\/(?\d+)/g; - - const results = message.content.matchAll(re); - const resultEmbeds: EmbedBuilder[] = []; - - for (const r of results) { - if (resultEmbeds.length >= 3) break; // only process three previews - - if (r.groups == undefined || r.groups.serverId != message.guildId) continue; // do not let the bot leak messages from one server to another - - try { - const channel = await message.guild?.channels.fetch(r.groups.channelId); - - if (!channel || !channel.isTextBased()) continue; - - if (channel instanceof ThreadChannel) { - if ( - !channel.parent?.members?.some((user) => user.id == message.author.id) - ) - continue; // do not reveal a message to a user who can't see it - } else { - if (!channel.members?.some((user) => user.id == message.author.id)) - continue; // do not reveal a message to a user who can't see it - } - - const originalMessage = await channel.messages.fetch(r.groups.messageId); - - const embed = new EmbedBuilder() - .setAuthor({ - name: originalMessage.author.tag, - iconURL: originalMessage.author.displayAvatarURL(), - }) - .setColor(Colors.Aqua) - .setTimestamp(originalMessage.createdTimestamp) - .setFooter({ text: `#${originalMessage.channel.name}` }); - - embed.setDescription( - (originalMessage.content ? originalMessage.content + '\n\n' : '') + - `[Jump to original message](${originalMessage.url})` - ); - - if (originalMessage.attachments.size > 0) { - embed.addFields({ - name: 'Attachments', - value: originalMessage.attachments - .map((att) => `[${att.name}](${att.url})`) - .join('\n'), - }); - - const firstImage = findFirstImage(originalMessage); - if (firstImage) { - embed.setImage(firstImage); - } - } - - resultEmbeds.push(embed); - } catch (ignored) { - /* */ - } - } - - if (resultEmbeds.length > 0) { - const reply = await message.reply({ - embeds: resultEmbeds, - allowedMentions: { repliedUser: false }, - }); - - const collector = new ReactionCollector(reply, { - filter: (reaction) => { - return reaction.emoji.name === '❌'; - }, - time: 5 * 60 * 1000, - }); - - collector.on('collect', async (_, user) => { - if (user === message.author) { - await reply.delete(); - collector.stop(); - } - }); - } -} diff --git a/tags/always-offline.md b/tags/always_offline.md similarity index 96% rename from tags/always-offline.md rename to tags/always_offline.md index a1a12d3..56b64ea 100644 --- a/tags/always-offline.md +++ b/tags/always_offline.md @@ -1,7 +1,6 @@ --- title: Prism Launcher is always offline? color: orange -aliases: ['reinstall'] --- If you recently updated Prism Launcher on Windows and you are now unable to do anything in Prism Launcher that would require an internet connection (i.e. logging in, downloading mods/modpacks and more), try uninstalling the launcher and then reinstalling it again. diff --git a/tags/binary-search.md b/tags/binary_search.md similarity index 96% rename from tags/binary-search.md rename to tags/binary_search.md index 5472343..487a902 100644 --- a/tags/binary-search.md +++ b/tags/binary_search.md @@ -1,7 +1,6 @@ --- title: Binary Search - A method of finding problems with mods color: blue -aliases: ['thanosmethod'] --- The binary search is a way of finding a faulty thing amongst a lot of other things, without having to remove the things one-by-one. This is useful for finding a broken mod among hundreds of mods, without having to spend time testing the mods one-by-one. diff --git a/tags/curseforge.md b/tags/curseforge.md index b2b8e78..cd67b98 100644 --- a/tags/curseforge.md +++ b/tags/curseforge.md @@ -1,7 +1,6 @@ --- title: What's wrong with CurseForge? color: orange -aliases: ['cf', 'curse', 'cursed', 'cursedfrog'] --- CurseForge added a new option to block third party clients like Prism Launcher from accessing mod files, and they started to enforce this option lately. We can't allow you to download those mods directly from CurseForge because of this. However, Prism Launcher offers a workaround to enable the downloading of these mods, by allowing you to download these mods from your browser and automatically importing them into the instance. We highly encourage asking authors that opted out of client downloads to stop doing so. diff --git a/tags/fractureiser.md b/tags/fractureiser.md index 394f667..2a124d7 100644 --- a/tags/fractureiser.md +++ b/tags/fractureiser.md @@ -1,7 +1,6 @@ --- title: Information about Fractureiser color: orange -aliases: ['malware'] --- Starting June 6th, 2023 UTC, there have been reports of malware being distributed through websites such as Curseforge, Bukkit, and possibly others in the form of mods, plugins, and modpacks. According to both [Modrinth](https://twitter.com/modrinth/status/1666853947804463115) and [Curseforge](https://twitter.com/CurseForge/status/1666741580022128641), all infected files have been removed. These services should be safe to use now, however users should still take caution in downloading files, especially from less trustworthy services. diff --git a/tags/FTB.md b/tags/ftb.md similarity index 97% rename from tags/FTB.md rename to tags/ftb.md index 4e49dbc..f78a016 100644 --- a/tags/FTB.md +++ b/tags/ftb.md @@ -1,7 +1,6 @@ --- title: Can I install/download FTB packs directly from Prism? color: orange -aliases: ['FTB'] --- You cannot download FTB packs directly from Prism Launcher. diff --git a/tags/javaforgebugfix.md b/tags/java_forge_bugfix.md similarity index 85% rename from tags/javaforgebugfix.md rename to tags/java_forge_bugfix.md index 9433d79..5b85315 100644 --- a/tags/javaforgebugfix.md +++ b/tags/java_forge_bugfix.md @@ -1,7 +1,6 @@ --- title: Forge Bugfix color: yellow -aliases: ['javaforgebug', 'forgebug', 'forgejavabugfix', 'forgejavabug'] image: https://media.discordapp.net/attachments/1040383700845740072/1057840239751729172/Fix.png --- diff --git a/tags/legacyjavafixer.md b/tags/legacy_java_fixer.md similarity index 100% rename from tags/legacyjavafixer.md rename to tags/legacy_java_fixer.md diff --git a/tags/log.md b/tags/log.md index c0a9756..94a3cf6 100644 --- a/tags/log.md +++ b/tags/log.md @@ -1,7 +1,6 @@ --- title: Upload Logs color: orange -aliases: ['sendlog', 'logs', '🪵'] image: https://cdn.discordapp.com/attachments/1031694870756204566/1156971972232740874/image.png --- diff --git a/tags/macosarmjava.md b/tags/macos_arm_java.md similarity index 95% rename from tags/macosarmjava.md rename to tags/macos_arm_java.md index a2b5af3..cf3f406 100644 --- a/tags/macosarmjava.md +++ b/tags/macos_arm_java.md @@ -1,7 +1,6 @@ --- title: Install arm64 Java on macOS color: yellow -aliases: ['armmacosjava'] --- On macOS arm64, also known as Apple Silicon (M1, M2, etc.), you will need to install the arm64 version of Java. We recommend using builds from Azul, the links for which can be found below: diff --git a/tags/migrate.md b/tags/migrate.md index 4045365..16886f3 100644 --- a/tags/migrate.md +++ b/tags/migrate.md @@ -1,7 +1,6 @@ --- title: Migrating from MultiMC color: orange -aliases: ['migr', 'mmc', 'multimc'] --- https://prismlauncher.org/wiki/getting-started/migrating-multimc/ diff --git a/tags/nightly.md b/tags/nightly.md index 0c83907..475feb6 100644 --- a/tags/nightly.md +++ b/tags/nightly.md @@ -1,7 +1,6 @@ --- title: Where can I download unstable builds of Prism Launcher? color: green -aliases: ['unstable'] --- You can download unstable builds [here](https://nightly.link/PrismLauncher/PrismLauncher/workflows/trigger_builds/develop). diff --git a/tags/optifine.md b/tags/optifine.md index 4d0271e..403e099 100644 --- a/tags/optifine.md +++ b/tags/optifine.md @@ -1,7 +1,6 @@ --- title: OptiFine color: green -aliases: ['of', 'optimize', 'opticrap', 'notfine'] --- OptiFine is known to cause problems when paired with other mods. diff --git a/tags/paths.md b/tags/paths.md index 693db03..6ae5961 100644 --- a/tags/paths.md +++ b/tags/paths.md @@ -1,7 +1,6 @@ --- title: Data directories color: blue -aliases: ['dirs', 'locate'] fields: - name: Portable (Windows / Linux) diff --git a/tags/pluralkit.md b/tags/pluralkit.md index b7f34d5..3334af9 100644 --- a/tags/pluralkit.md +++ b/tags/pluralkit.md @@ -1,7 +1,6 @@ --- title: Why PluralKit? color: blue -aliases: ['pk'] --- Plurality is the existence of multiple self-aware entities inside the same brain. diff --git a/tags/update.md b/tags/update.md index 3d022b9..ec17f2a 100644 --- a/tags/update.md +++ b/tags/update.md @@ -1,7 +1,6 @@ --- title: Does Prism Launcher auto-update? color: blue -aliases: ['updating', 'autoupdate'] --- Windows auto-updating is only supported on 8.0+. For Prism 7.2 or below, you will need to download the installer and run it again in order to update. On 8.0 or newer, click the 'Update' button. You will not lose your instances. diff --git a/tags/vcredist.md b/tags/vcredist.md index 23af9d6..c71ed9c 100644 --- a/tags/vcredist.md +++ b/tags/vcredist.md @@ -1,7 +1,6 @@ --- title: vcredist is required for Prism to run Windows color: pink -aliases: ['msvc'] --- Like most apps on Windows, you have to install vcredist for Prism to run. Depending on what version of Prism you are using, you may need a different version. diff --git a/tags/why.md b/tags/why.md index cc31d2e..77d5cf0 100644 --- a/tags/why.md +++ b/tags/why.md @@ -1,12 +1,6 @@ --- title: But why? color: purple -aliases: - - 'whywasprismlaunchermade' - - 'whywasprismmade' - - 'whywaspolymcmade' - - 'mmcdrama' - - 'devlauncher' --- https://prismlauncher.org/wiki/overview/faq/#why-did-our-community-choose-to-fork diff --git a/tags/whyjava8.md b/tags/why_java_8.md similarity index 92% rename from tags/whyjava8.md rename to tags/why_java_8.md index d069e49..f4eac04 100644 --- a/tags/whyjava8.md +++ b/tags/why_java_8.md @@ -1,7 +1,6 @@ --- title: Why does Prism Launcher ask me to change Java version? color: orange -aliases: ['isjava8ancient', 'whyisprismforcingme'] --- Minecraft versions before 1.17 required Java 8 and have issues with newer Java, while newer versions require Java 17, so you need to change Java version. Some people think Java 8 is very outdated, however, it's actually an LTS, meaning it's still getting updates. diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index def0a5e..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "strict": true, - "downlevelIteration": true, - "module": "esnext", - "target": "esnext", - "moduleResolution": "node", - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "noEmit": true - } -}