diff options
67 files changed, 1296 insertions, 1173 deletions
diff --git a/.codespellrc b/.codespellrc deleted file mode 100644 index 794d507..0000000 --- a/.codespellrc +++ /dev/null @@ -1,2 +0,0 @@ -[codespell] -ignore-words = crate diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e24cebd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = tab +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{lock,nix,yml,yaml}] +indent_style = space +indent_size = 2 diff --git a/.env.template b/.env.template index 60492e1..6223b8c 100644 --- a/.env.template +++ b/.env.template @@ -1 +1,5 @@ -TOKEN=AAAAAAAAAAAAAAAAAAAAAAAAAA.AMOGUS.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA +TOKEN= +REDIS_URL= + +RUST_BACKTRACE=1 +RUST_LOG="teawiebot=debug,warn" @@ -1,11 +1,6 @@ -# only use flake when `nix` is present -if command -v nix &> /dev/null; then - if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then - source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs=" - fi - - watch_file ./parts/dev.nix - use flake +# only use flake when `nix-direnv` is installed +if has nix_direnv_version; then + use flake ./nix/dev fi dotenv_if_exists diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index fcadb2c..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -* text eol=lf diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ea75cc8..f9f0b67 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,11 +5,11 @@ updates: schedule: interval: "weekly" commit-message: - prefix: "deps(actions)" + prefix: "ci" - package-ecosystem: "cargo" directory: "/" schedule: interval: "weekly" commit-message: - prefix: "deps(crates)" + prefix: "crates" diff --git a/.github/workflows/autobot.yaml b/.github/workflows/autobot.yaml index a8b959f..69d6ad0 100644 --- a/.github/workflows/autobot.yaml +++ b/.github/workflows/autobot.yaml @@ -5,14 +5,14 @@ on: pull_request jobs: automerge: name: Check and merge PR + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-latest permissions: contents: write pull-requests: write - if: github.actor == 'dependabot[bot]' - steps: - uses: dependabot/fetch-metadata@v2 id: metadata diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..67371c0 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,114 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout repository + 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: Run build + run: cargo build --locked --release + + clippy: + name: Run Clippy scan + + runs-on: ubuntu-latest + + permissions: + security-events: write + + 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: Setup Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install SARIF tools + run: | + nix profile install \ + --inputs-from ./nix/dev \ + github:getchoo/nix-exprs#{clippy-sarif,sarif-fmt} + + - name: Fetch Cargo deps + run: | + nix develop ./nix/dev#ci --command \ + cargo fetch --locked + + - name: Run Clippy + continue-on-error: true + run: | + nix develop ./nix/dev#ci --command \ + 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 + + format: + name: Check formatting + + runs-on: ubuntu-latest + + 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 treefmt + run: | + pushd nix/dev + nix fmt + popd + git diff --color=always --exit-code + + release-gate: + name: CI Release Gate + needs: [build, format] + + runs-on: ubuntu-latest + + steps: + - name: Exit with result + run: echo "We're good to go!" diff --git a/.github/workflows/clippy.yaml b/.github/workflows/clippy.yaml deleted file mode 100644 index 1c3a316..0000000 --- a/.github/workflows/clippy.yaml +++ /dev/null @@ -1,49 +0,0 @@ -name: Clippy - -on: - push: - branches: ["main"] - pull_request: - -jobs: - 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 -euxo 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.yaml b/.github/workflows/docker.yaml index d738366..5f45bec 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -1,27 +1,25 @@ -name: Push to image registry +name: Docker on: - check_suite: - types: [completed] + push: + branches: [main] + pull_request: workflow_dispatch: jobs: build: name: Build image - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: arch: [x86_64, aarch64] - # https://github.com/sellout/bash-strict-mode/commit/9bf1d65c2f786a9887facfcb81e06d8b8b5f4667 - if: github.event.check_suite.app.name == 'Garnix CI' - && github.event.check_suite.conclusion == 'success' - && github.event.check_suite.latest_check_runs_count >= 8 - && github.event.check_suite.head_branch == 'main' + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Install Nix uses: DeterminateSystems/nix-installer-action@v10 @@ -31,10 +29,17 @@ jobs: - name: Build Docker image id: build + env: + ARCH: ${{ matrix.arch }} run: | - nix build -L --accept-flake-config .#container-${{ matrix.arch }} + nix build \ + --fallback \ + --print-build-logs \ + ./nix/dev#container-"$ARCH" + + # exit if no `result` from nix build [ ! -L result ] && exit 1 - echo "path=$(realpath result)" >> "$GITHUB_OUTPUT" + echo "path=$(readlink -f ./result)" >> "$GITHUB_OUTPUT" - name: Upload image uses: actions/upload-artifact@v4 @@ -44,10 +49,21 @@ jobs: if-no-files-found: error retention-days: 1 + release-gate: + name: Docker Release Gate + needs: build + + runs-on: ubuntu-latest + + steps: + - name: Exit with result + run: echo "We're good to go!" + push: name: Push image + if: github.event_name == 'push' + needs: release-gate - needs: build runs-on: ubuntu-latest permissions: @@ -55,14 +71,15 @@ jobs: env: REGISTRY: ghcr.io - USERNAME: getchoo + USERNAME: ${{ github.actor }} steps: - name: Set image name run: | echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Download images uses: actions/download-artifact@v4 @@ -80,17 +97,15 @@ jobs: env: TAG: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest run: | - set -eux - architectures=("x86_64" "aarch64") for arch in "${architectures[@]}"; do docker load < images/container-"$arch"/*.tar.gz - docker tag teawiebot:latest-"$arch" ${{ env.TAG }}-"$arch" - docker push ${{ env.TAG }}-"$arch" + docker tag teawiebot:latest-"$arch" "$TAG"-"$arch" + docker push "$TAG"-"$arch" done - docker manifest create ${{ env.TAG }} \ - --amend ${{ env.TAG }}-x86_64 \ - --amend ${{ env.TAG }}-aarch64 + docker manifest create "$TAG" \ + --amend "$TAG"-x86_64 \ + --amend "$TAG"-aarch64 - docker manifest push ${{ env.TAG }} + docker manifest push "$TAG" diff --git a/.github/workflows/nix.yaml b/.github/workflows/nix.yaml new file mode 100644 index 0000000..4427afa --- /dev/null +++ b/.github/workflows/nix.yaml @@ -0,0 +1,64 @@ +name: Nix + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build + + strategy: + fail-fast: false + 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 build + run: nix build --fallback --print-build-logs + + check: + name: Check flake + + runs-on: ubuntu-latest + + 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 nix flake check + run: | + nix flake check \ + --all-systems \ + --fallback \ + --print-build-logs \ + --show-trace + + release-gate: + name: Nix Release Gate + needs: [build, check] + + runs-on: ubuntu-latest + + steps: + - name: Exit with result + run: echo "We're good to go!" diff --git a/.github/workflows/update-flake.yaml b/.github/workflows/update-flake.yaml index 3726908..a3a6293 100644 --- a/.github/workflows/update-flake.yaml +++ b/.github/workflows/update-flake.yaml @@ -1,4 +1,4 @@ -name: Update flake.lock +name: Update lockfiles on: schedule: @@ -8,29 +8,65 @@ on: jobs: update: + name: Run update runs-on: ubuntu-latest permissions: contents: write pull-requests: write + env: + PR_BRANCH: "update-lockfiles" + steps: - - uses: actions/checkout@v4 + - name: Checkout repository + uses: actions/checkout@v4 - name: Install Nix uses: DeterminateSystems/nix-installer-action@v10 - - name: Update lockfile & make PR - uses: DeterminateSystems/update-flake-lock@v21 - id: update - with: - commit-msg: "flake: update inputs" - pr-title: "flake: update inputs" - token: ${{ github.token }} + - 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 --rebase "$PR_ID" + run: gh pr merge --auto --squash env: - GH_TOKEN: ${{ github.token }} - PR_ID: ${{ steps.update.outputs.pull-request-number }} + GH_TOKEN: ${{ secrets.MERGE_TOKEN }} diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 301d47e..0000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -flake.lock @@ -304,7 +304,7 @@ dependencies = [ "eyre", "indenter", "once_cell", - "owo-colors 3.5.0", + "owo-colors", "tracing-error", ] @@ -315,7 +315,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" dependencies = [ "once_cell", - "owo-colors 3.5.0", + "owo-colors", "tracing-core", "tracing-error", ] @@ -1169,12 +1169,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] -name = "owo-colors" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f" - -[[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2067,8 +2061,6 @@ dependencies = [ "eyre", "include_dir", "log", - "once_cell", - "owo-colors 4.0.0", "poise", "rand 0.8.5", "redis 0.25.2", @@ -5,7 +5,6 @@ edition = "2021" repository = "https://github.com/getchoo/teawieBot" license = "MIT" readme = "README.md" -build = "build.rs" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -14,12 +13,13 @@ bottomify = "1.2.0" color-eyre = "0.6.3" dotenvy = "0.15.7" env_logger = "0.11.3" -eyre = "0.6.12" +eyre = { version = "0.6.12", default-features = false, features = [ + "auto-install", + "track-caller", +] } include_dir = "0.7.3" log = "0.4.21" poise = "0.6.1" -once_cell = "1.19.0" -owo-colors = "4.0.0" rand = "0.8.5" redis = { version = "0.25.2", features = ["tokio-comp", "tokio-rustls-comp"] } redis-macros = "0.2.1" @@ -36,3 +36,14 @@ tokio = { version = "1.37.0", features = [ ] } url = { version = "2.5.0", features = ["serde"] } uwurandom-rs = "1.1.0" + +[lints.rust] +unsafe_code = "forbid" + +[lints.clippy] +complexity = "warn" +correctness = "deny" +pedantic = "warn" +perf = "warn" +style = "warn" +suspicious = "deny" @@ -1,6 +1,6 @@ # teawie bot đŠđŠđŠ -[](https://garnix.io) +[](https://github.com/getchoo/teawieBot/actions/workflows/ci.yaml) okay so like basically, it's just a discord bot named "teawie" (so cool!! and now in rust!!!)đđ diff --git a/build.rs b/build.rs deleted file mode 100644 index af1183f..0000000 --- a/build.rs +++ /dev/null @@ -1,6 +0,0 @@ -fn main() { - println!( - "cargo:rustc-env=TARGET={}", - std::env::var("TARGET").unwrap() - ); -} @@ -1,85 +1,5 @@ { "nodes": { - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1711606966, - "narHash": "sha256-nTaO7ZDL4D02dVC5ktqnXNiNuODBUHyE4qEcFjAUCQY=", - "owner": "nix-community", - "repo": "fenix", - "rev": "aa45c3e901ea42d6633af083c0c555efaf948b17", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, - "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" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "pre-commit-hooks-nix", - "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, @@ -96,112 +16,9 @@ "type": "github" } }, - "pre-commit-hooks-nix": { - "inputs": { - "flake-compat": [], - "flake-utils": "flake-utils", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ], - "nixpkgs-stable": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1711760291, - "narHash": "sha256-GWIBVrHnQQYtZA4aaS3FQebthxvL0Uz4ifJSvJxblfw=", - "owner": "cachix", - "repo": "pre-commit-hooks.nix", - "rev": "0d676ca9ca9df7f2d4d5fb0de511fed3a4b67fdf", - "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": { - "fenix": "fenix", - "flake-parts": "flake-parts", - "nixpkgs": "nixpkgs", - "pre-commit-hooks-nix": "pre-commit-hooks-nix", - "procfile-nix": "procfile-nix", - "treefmt-nix": "treefmt-nix" - } - }, - "rust-analyzer-src": { - "flake": false, - "locked": { - "lastModified": 1711562745, - "narHash": "sha256-s/YOyBM0vumhkqCFi8CnV5imFlC5JJrGia8CmEXyQkM=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "ad51a17c627b4ca57f83f0dc1f3bb5f3f17e6d0b", - "type": "github" - }, - "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", - "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": 1711531821, - "narHash": "sha256-5n4hq7PsH8g9czJ5HvXpVrJ4AiJdzrutHK01oKIaCXE=", - "owner": "numtide", - "repo": "treefmt-nix", - "rev": "c2172ef83d6904cdff3118e0c08e89171db6028a", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "treefmt-nix", - "type": "github" + "nixpkgs": "nixpkgs" } } }, @@ -1,62 +1,35 @@ { description = "teawie moment"; - nixConfig = { - extra-substituters = ["https://cache.garnix.io"]; - extra-trusted-public-keys = ["cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g="]; - }; - - inputs = { - nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - - flake-parts = { - url = "github:hercules-ci/flake-parts"; - inputs.nixpkgs-lib.follows = "nixpkgs"; - }; - - fenix = { - url = "github:nix-community/fenix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - pre-commit-hooks-nix = { - url = "github:cachix/pre-commit-hooks.nix"; - inputs = { - nixpkgs.follows = "nixpkgs"; - nixpkgs-stable.follows = "nixpkgs"; - flake-compat.follows = ""; - }; - }; - - treefmt-nix = { - url = "github:numtide/treefmt-nix"; - inputs.nixpkgs.follows = "nixpkgs"; - }; - - procfile-nix = { - url = "github:getchoo/procfile-nix"; - inputs.nixpkgs.follows = "nixpkgs"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + outputs = { + self, + nixpkgs, + ... + }: let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); + in { + nixosModules.default = import ./nix/module.nix self; + + packages = forAllSystems ({ + pkgs, + system, + ... + }: { + teawiebot = pkgs.callPackage ./nix/derivation.nix {inherit self;}; + default = self.packages.${system}.teawiebot; + }); + + overlays.default = _: prev: { + teawiebot = prev.callPackage ./nix/derivation.nix {inherit self;}; }; }; - - outputs = inputs: - inputs.flake-parts.lib.mkFlake {inherit inputs;} { - imports = [ - inputs.pre-commit-hooks-nix.flakeModule - inputs.procfile-nix.flakeModule - inputs.treefmt-nix.flakeModule - - ./nix/ci.nix - ./nix/deployment - ./nix/dev.nix - ./nix/packages.nix - ]; - - systems = [ - "x86_64-linux" - "x86_64-darwin" - "aarch64-linux" - "aarch64-darwin" - ]; - }; } diff --git a/garnix.yaml b/garnix.yaml deleted file mode 100644 index 64bee81..0000000 --- a/garnix.yaml +++ /dev/null @@ -1,6 +0,0 @@ -builds: - exclude: [] - include: - - "checks.x86_64-linux.*" - - "packages.x86_64-linux.*" - - "devShells.x86_64-linux.default" diff --git a/nix/ci.nix b/nix/ci.nix deleted file mode 100644 index 682f46f..0000000 --- a/nix/ci.nix +++ /dev/null @@ -1,23 +0,0 @@ -{ - perSystem = { - pkgs, - lib, - self', - ... - }: { - /* - require packages, checks, and devShells for ci to be considered a success - - also thanks DetSys for showing me i don't need to use runCommand, symlinkJoin, or linkFarm! - https://determinate.systems/posts/hydra-deployment-source-of-truth - */ - - packages.ciGate = pkgs.writeText "ci-gate" ( - lib.concatMapStringsSep "\n" (s: toString (lib.attrValues s)) [ - self'.checks - self'.devShells - (builtins.removeAttrs self'.packages ["default" "ciGate"]) - ] - ); - }; -} diff --git a/nix/deployment/static.nix b/nix/deployment/static.nix deleted file mode 100644 index dcdf0f3..0000000 --- a/nix/deployment/static.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ - perSystem = { - lib, - pkgs, - inputs', - self', - ... - }: let - targets = with pkgs.pkgsCross; { - x86_64 = musl64.pkgsStatic; - aarch64 = aarch64-multiplatform.pkgsStatic; - }; - - toolchain = let - fenix = inputs'.fenix.packages; - in - with fenix; - combine ( - [minimal.cargo minimal.rustc] - ++ map ( - pkgs: - fenix.targets.${pkgs.stdenv.hostPlatform.config}.latest.rust-std - ) (lib.attrValues targets) - ); - - rustPlatforms = - lib.mapAttrs ( - lib.const (pkgs: - pkgs.makeRustPlatform ( - lib.genAttrs ["cargo" "rustc"] (lib.const toolchain) - )) - ) - targets; - - buildTeawieWith = rustPlatform: - self'.packages.teawiebot.override { - inherit rustPlatform; - lto = true; - optimizeSize = true; - }; - in { - packages = lib.optionalAttrs pkgs.stdenv.isLinux ( - lib.mapAttrs' ( - target: rustPlatform: - lib.nameValuePair "teawiebot-static-${target}" (buildTeawieWith rustPlatform) - ) - rustPlatforms - ); - }; -} diff --git a/nix/derivation.nix b/nix/derivation.nix index bb60706..fa4867a 100644 --- a/nix/derivation.nix +++ b/nix/derivation.nix @@ -4,7 +4,7 @@ rustPlatform, darwin, self, - lto ? false, + lto ? true, optimizeSize ? false, }: rustPlatform.buildRustPackage { @@ -21,7 +21,6 @@ rustPlatform.buildRustPackage { ../src ../Cargo.toml ../Cargo.lock - ../build.rs ]; }; @@ -33,30 +32,28 @@ rustPlatform.buildRustPackage { CoreFoundation Security SystemConfiguration + darwin.libiconv ]); - env = { - GIT_SHA = self.shortRev or self.dirtyShortRev or "unknown-dirty"; - CARGO_BUILD_RUSTFLAGS = lib.concatStringsSep " " ( - lib.optionals lto [ - "-C" - "lto=thin" - "-C" - "embed-bitcode=yes" - "-Zdylib-lto" - ] - ++ lib.optionals optimizeSize [ - "-C" - "codegen-units=1" - "-C" - "panic=abort" - "-C" - "strip=symbols" - "-C" - "opt-level=z" - ] + env = let + toRustFlags = lib.mapAttrs' ( + name: + lib.nameValuePair + "CARGO_BUILD_RELEASE_${lib.toUpper (builtins.replaceStrings ["-"] ["_"] name)}" ); - }; + in + { + GIT_SHA = self.shortRev or self.dirtyShortRev or "unknown-dirty"; + } + // lib.optionalAttrs lto (toRustFlags { + lto = "thin"; + }) + // lib.optionalAttrs optimizeSize (toRustFlags { + codegen-units = 1; + opt-level = "s"; + panic = "abort"; + strip = "symbols"; + }); meta = with lib; { mainProgram = "teawiebot"; diff --git a/nix/dev.nix b/nix/dev.nix deleted file mode 100644 index d9f15d4..0000000 --- a/nix/dev.nix +++ /dev/null @@ -1,71 +0,0 @@ -{ - perSystem = { - lib, - pkgs, - config, - self', - ... - }: let - enableAll = lib.flip lib.genAttrs (lib.const {enable = true;}); - in { - treefmt = { - projectRootFile = "flake.nix"; - - programs = enableAll [ - "alejandra" - "deadnix" - "prettier" - "rustfmt" - ]; - - settings.global = { - excludes = [ - "./target" - "./flake.lock" - "./Cargo.lock" - ]; - }; - }; - - pre-commit.settings = { - settings.treefmt.package = config.treefmt.build.wrapper; - - hooks = enableAll [ - "actionlint" - "nil" - "statix" - "treefmt" - ]; - }; - - procfiles.daemons.processes = { - redis = lib.getExe' pkgs.redis "redis-server"; - }; - - devShells = { - default = pkgs.mkShell { - packages = with pkgs; [ - # general - actionlint - nodePackages_latest.prettier - config.procfiles.daemons.package - - # rust - cargo - rustc - clippy - rustfmt - rust-analyzer - - # nix - self'.formatter - deadnix - nil - statix - ]; - - RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; - }; - }; - }; -} diff --git a/nix/dev/checks.nix b/nix/dev/checks.nix new file mode 100644 index 0000000..1d65625 --- /dev/null +++ b/nix/dev/checks.nix @@ -0,0 +1,27 @@ +{ + perSystem = { + lib, + pkgs, + ... + }: { + checks = { + actionlint = pkgs.runCommand "check-actionlint" {} '' + ${lib.getExe pkgs.actionlint} ${../../.github/workflows}/* + touch $out + ''; + + editorconfig = pkgs.runCommand "check-editorconfig" {} '' + cd ${../../.} + ${lib.getExe pkgs.editorconfig-checker} \ + -exclude '.git' . + + touch $out + ''; + + statix = pkgs.runCommand "check-statix" {} '' + ${lib.getExe pkgs.statix} check ${../../.} + touch $out + ''; + }; + }; +} diff --git a/nix/deployment/default.nix b/nix/dev/docker.nix index 7fd379f..b209015 100644 --- a/nix/deployment/default.nix +++ b/nix/dev/docker.nix @@ -1,14 +1,4 @@ -{ - flake-parts-lib, - withSystem, - ... -}: { - imports = [./static.nix]; - - flake.nixosModules.default = flake-parts-lib.importApply ./module.nix { - inherit withSystem; - }; - +{withSystem, ...}: { perSystem = { lib, pkgs, diff --git a/nix/dev/flake.lock b/nix/dev/flake.lock new file mode 100644 index 0000000..881402a --- /dev/null +++ b/nix/dev/flake.lock @@ -0,0 +1,145 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1712384501, + "narHash": "sha256-AZmYmEnc1ZkSlxUJVUtGh9VFAqWPr+xtNIiBqD2eKfc=", + "owner": "nix-community", + "repo": "fenix", + "rev": "99c6241db5ca5363c05c8f4acbdf3a4e8fc42844", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712014858, + "narHash": "sha256-sB4SWl2lX95bExY2gMFG5HIzvva5AVMJd4Igm+GpZNw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "9126214d0a59633752a136528f5f3b9aa8565b7d", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "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" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1712512205, + "narHash": "sha256-CrKKps0h7FoagRcE2LT3h/72Z64D0Oh83UF1XZVhCLQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "3c1b6f75344e207a67536d834886ee9b4577ebe7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "procfile-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1712476251, + "narHash": "sha256-o6L0IDUy5ri1ijXP6D132gYcSYT7JweNQr5P9DOozRM=", + "owner": "getchoo", + "repo": "procfile-nix", + "rev": "c3320b59670450b4d2b5cb01cdbd3e176a6d3232", + "type": "github" + }, + "original": { + "owner": "getchoo", + "repo": "procfile-nix", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-parts": "flake-parts", + "get-flake": "get-flake", + "nixpkgs": "nixpkgs", + "procfile-nix": "procfile-nix", + "treefmt-nix": "treefmt-nix" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1712156296, + "narHash": "sha256-St7ZQrkrr5lmQX9wC1ZJAFxL8W7alswnyZk9d1se3Us=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "8e581ac348e223488622f4d3003cb2bd412bf27e", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1711963903, + "narHash": "sha256-N3QDhoaX+paWXHbEXZapqd1r95mdshxToGowtjtYkGI=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "49dc4a92b02b8e68798abd99184f228243b6e3ac", + "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..1442839 --- /dev/null +++ b/nix/dev/flake.nix @@ -0,0 +1,62 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + + flake-parts = { + url = "github:hercules-ci/flake-parts"; + inputs.nixpkgs-lib.follows = "nixpkgs"; + }; + + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + get-flake.url = "github:ursi/get-flake"; + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + + procfile-nix = { + url = "github:getchoo/procfile-nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} { + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + imports = [ + # dev utils + ./checks.nix + ./procfile.nix + ./shell.nix + ./treefmt.nix + + # special, private builds + ./docker.nix + ./static.nix + + inputs.treefmt-nix.flakeModule + inputs.procfile-nix.flakeModule + ]; + + perSystem = { + lib, + system, + ... + }: { + _module.args = { + teawiebot' = lib.mapAttrs (lib.const (v: v.${system} or v)) (inputs.get-flake ../../.); + }; + }; + }; +} diff --git a/nix/dev/procfile.nix b/nix/dev/procfile.nix new file mode 100644 index 0000000..4b1c665 --- /dev/null +++ b/nix/dev/procfile.nix @@ -0,0 +1,13 @@ +{ + 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..c5589ef --- /dev/null +++ b/nix/dev/shell.nix @@ -0,0 +1,45 @@ +{ + perSystem = { + config, + pkgs, + self', + teawiebot', + ... + }: { + devShells = { + default = pkgs.mkShell { + packages = [ + # rust tools + pkgs.clippy + pkgs.rustfmt + pkgs.rust-analyzer + + # nix tools + pkgs.deadnix + pkgs.nil + pkgs.statix + + # misc formatter/linters + pkgs.actionlint + self'.formatter + + config.procfiles.daemons.package + ]; + + inputsFrom = [teawiebot'.packages.teawiebot]; + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; + + ci = pkgs.mkShell { + packages = [ + pkgs.clippy + pkgs.rustfmt + + self'.formatter + ]; + + inputsFrom = [teawiebot'.packages.teawiebot]; + }; + }; + }; +} diff --git a/nix/dev/static.nix b/nix/dev/static.nix new file mode 100644 index 0000000..e8780f4 --- /dev/null +++ b/nix/dev/static.nix @@ -0,0 +1,39 @@ +{ + perSystem = { + lib, + pkgs, + inputs', + teawiebot', + ... + }: let + crossTargets = with pkgs.pkgsCross; { + x86_64 = musl64.pkgsStatic; + aarch64 = aarch64-multiplatform.pkgsStatic; + }; + + rustStdFor = pkgs: inputs'.fenix.packages.targets.${pkgs.stdenv.hostPlatform.rust.rustcTarget}.stable.rust-std; + toolchain = with inputs'.fenix.packages; + combine (lib.flatten [ + stable.cargo + stable.rustc + (map rustStdFor (lib.attrValues crossTargets)) + ]); + + rustPlatformFor = pkgs: + pkgs.makeRustPlatform ( + lib.genAttrs ["cargo" "rustc"] (lib.const toolchain) + ); + crossPlatforms = lib.mapAttrs (lib.const rustPlatformFor) crossTargets; + + buildTeawieWith = rustPlatform: + teawiebot'.packages.teawiebot.override { + inherit rustPlatform; + optimizeSize = true; + }; + in { + packages = { + teawiebot-static-x86_64 = buildTeawieWith crossPlatforms.x86_64; + teawiebot-static-aarch64 = buildTeawieWith crossPlatforms.aarch64; + }; + }; +} diff --git a/nix/dev/treefmt.nix b/nix/dev/treefmt.nix new file mode 100644 index 0000000..5e1fd1f --- /dev/null +++ b/nix/dev/treefmt.nix @@ -0,0 +1,21 @@ +{ + perSystem = { + treefmt = { + projectRootFile = "flake.nix"; + + programs = { + alejandra.enable = true; + deadnix.enable = true; + rustfmt.enable = true; + }; + + settings.global = { + excludes = [ + "./target" + "./flake.lock" + "./Cargo.lock" + ]; + }; + }; + }; +} diff --git a/nix/deployment/module.nix b/nix/module.nix index 09999f1..c129e68 100644 --- a/nix/deployment/module.nix +++ b/nix/module.nix @@ -1,4 +1,4 @@ -{withSystem, ...}: { +self: { config, lib, pkgs, @@ -19,11 +19,13 @@ optionals types ; + + inherit (pkgs.stdenv.hostPlatform) system; in { options.services.teawiebot = { enable = mkEnableOption "teawiebot"; package = mkPackageOption ( - withSystem pkgs.stdenv.hostPlatform.system ({self', ...}: self'.packages) + self.packages.${system} or (builtins.throw "${system} is not supported!") ) "teawiebot" {}; user = mkOption { diff --git a/nix/packages.nix b/nix/packages.nix deleted file mode 100644 index f8087f9..0000000 --- a/nix/packages.nix +++ /dev/null @@ -1,12 +0,0 @@ -{self, ...}: { - perSystem = { - pkgs, - self', - ... - }: { - packages = { - teawiebot = pkgs.callPackage ./derivation.nix {inherit self;}; - default = self'.packages.teawiebot; - }; - }; -} diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs index 437dbe6..c5093da 100644 --- a/src/api/guzzle.rs +++ b/src/api/guzzle.rs @@ -1,31 +1,19 @@ -use crate::api::REQWEST_CLIENT; - -use eyre::{eyre, Result}; +use eyre::Result; use log::debug; -use reqwest::StatusCode; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] -struct GuzzleResponse { - pub url: String, +struct RandomTeawieResponse { + url: String, } const GUZZLE: &str = "https://api.mydadleft.me"; const RANDOM_TEAWIE: &str = "/random_teawie"; -pub async fn get_random_teawie() -> Result<String> { - let req = REQWEST_CLIENT - .get(format!("{GUZZLE}{RANDOM_TEAWIE}")) - .build()?; - - debug!("Making request to {}", req.url()); - let resp = REQWEST_CLIENT.execute(req).await?; - let status = resp.status(); +pub async fn random_teawie() -> Result<String> { + let url = format!("{GUZZLE}{RANDOM_TEAWIE}"); + debug!("Making request to {url}"); + let json: RandomTeawieResponse = super::get_json(&url).await?; - if let StatusCode::OK = status { - let data: GuzzleResponse = resp.json().await?; - Ok(data.url) - } else { - Err(eyre!("Failed to get random Teawie with {status}")) - } + Ok(json.url) } diff --git a/src/api/mod.rs b/src/api/mod.rs index 6554553..dac9209 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,17 +1,29 @@ -use once_cell::sync::Lazy; +use std::sync::OnceLock; + +use eyre::Result; +use reqwest::Client; +use serde::de::DeserializeOwned; pub mod guzzle; pub mod shiggy; -pub static USER_AGENT: Lazy<String> = Lazy::new(|| { - let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development"); +pub fn client() -> &'static Client { + static USER_AGENT: OnceLock<String> = OnceLock::new(); + static CLIENT: OnceLock<Client> = OnceLock::new(); + + let user_agent = USER_AGENT.get_or_init(|| { + let version = option_env!("CARGO_PKG_VERSION").unwrap_or("development"); + + format!("teawieBot/{version}") + }); + + CLIENT.get_or_init(|| Client::builder().user_agent(user_agent).build().unwrap()) +} - format!("teawieBot/{version}") -}); +async fn get_json<T: DeserializeOwned>(url: &str) -> Result<T> { + let resp = client().get(url).send().await?; + resp.error_for_status_ref()?; + let json = resp.json().await?; -pub static REQWEST_CLIENT: Lazy<reqwest::Client> = Lazy::new(|| { - reqwest::Client::builder() - .user_agent(USER_AGENT.to_string()) - .build() - .unwrap_or_default() -}); + Ok(json) +} diff --git a/src/api/shiggy.rs b/src/api/shiggy.rs index b1d4a87..d6a6238 100644 --- a/src/api/shiggy.rs +++ b/src/api/shiggy.rs @@ -1,8 +1,5 @@ -use crate::api::REQWEST_CLIENT; - -use eyre::{eyre, Result}; +use eyre::Result; use log::debug; -use reqwest::StatusCode; use serde::Deserialize; const SHIGGY: &str = "https://safebooru.donmai.us"; @@ -14,19 +11,10 @@ struct SafebooruResponse { } #[allow(clippy::module_name_repetitions)] -pub async fn get_random_shiggy() -> Result<String> { - let req = REQWEST_CLIENT - .get(format!("{SHIGGY}{RANDOM_SHIGGY}")) - .build()?; - - debug!("Making request to {}", req.url()); - let resp = REQWEST_CLIENT.execute(req).await?; - let status = resp.status(); +pub async fn random_shiggy() -> Result<String> { + let url = format!("{SHIGGY}{RANDOM_SHIGGY}"); + debug!("Making request to {url}"); - if let StatusCode::OK = status { - let data: SafebooruResponse = resp.json().await?; - Ok(data.file_url) - } else { - Err(eyre!("Failed to get random shiggy with {status}")) - } + let resp: SafebooruResponse = super::get_json(&url).await?; + Ok(resp.file_url) } diff --git a/src/colors.rs b/src/colors.rs deleted file mode 100644 index 2a3f402..0000000 --- a/src/colors.rs +++ /dev/null @@ -1,17 +0,0 @@ -use poise::serenity_prelude::Colour; - -pub enum Colors { - Blue, - Orange, - Red, -} - -impl From<Colors> for Colour { - fn from(val: Colors) -> Self { - match val { - Colors::Blue => Colour::from((136, 199, 253)), - Colors::Orange => Colour::from((255, 179, 74)), - Colors::Red => Colour::from((255, 94, 74)), - } - } -} diff --git a/src/commands/general/ask.rs b/src/commands/general/ask.rs index 75560e0..c715e3a 100644 --- a/src/commands/general/ask.rs +++ b/src/commands/general/ask.rs @@ -1,15 +1,16 @@ -use crate::{consts, utils, Context}; +use crate::{consts, utils, Context, Error}; -use eyre::{Context as _, Result}; +use eyre::Context as _; /// Ask teawie a question! #[poise::command(prefix_command, slash_command)] +#[allow(clippy::no_effect_underscore_binding)] pub async fn ask( ctx: Context<'_>, #[rename = "question"] #[description = "The question you want to ask teawie"] _question: String, -) -> Result<()> { +) -> Result<(), Error> { let resp = utils::random_choice(consts::RESPONSES) .wrap_err("Couldn't choose from random responses!")?; diff --git a/src/commands/general/bing.rs b/src/commands/general/bing.rs index d55d8ee..d58404e 100644 --- a/src/commands/general/bing.rs +++ b/src/commands/general/bing.rs @@ -1,10 +1,10 @@ -use crate::Context; +use crate::{Context, Error}; use eyre::Result; /// Make sure the wie is alive #[poise::command(prefix_command)] -pub async fn bing(ctx: Context<'_>) -> Result<()> { +pub async fn bing(ctx: Context<'_>) -> Result<(), Error> { ctx.say("bong!").await?; Ok(()) } diff --git a/src/commands/general/config.rs b/src/commands/general/config.rs index ddc5cda..456e791 100644 --- a/src/commands/general/config.rs +++ b/src/commands/general/config.rs @@ -1,7 +1,7 @@ -use std::str::FromStr; +use crate::storage::settings::{Properties, Settings}; +use crate::{Context, Error}; -use crate::{storage, Context}; -use storage::{Properties, Settings}; +use std::str::FromStr; use eyre::{OptionExt as _, Result}; use log::debug; @@ -41,7 +41,7 @@ fn prop_to_val(setting: &Properties, settings: &Settings) -> String { required_permissions = "MANAGE_GUILD", default_member_permissions = "MANAGE_GUILD" )] -pub async fn config(_ctx: Context<'_>) -> Result<()> { +pub async fn config(_: Context<'_>) -> Result<(), Error> { Ok(()) } @@ -72,63 +72,67 @@ pub async fn set( #[description = "Toggle ReactBoard"] reactboard_enabled: Option<bool>, #[description = "Enables 'extra' commands like teawiespam and copypasta. Defaults to false."] optional_commands_enabled: Option<bool>, -) -> Result<()> { - let storage = &ctx.data().storage; - let gid = ctx.guild_id().unwrap_or_default(); - let mut settings = storage.get_guild_settings(&gid).await?; - let previous_settings = settings.clone(); - - if let Some(channel) = pinboard_channel { - debug!("Setting pinboard_channel to {channel} for {gid}"); - settings.pinboard_channel = Some(channel.id); - } +) -> Result<(), Error> { + if let Some(storage) = &ctx.data().storage { + let gid = ctx.guild_id().unwrap_or_default(); + let mut settings = storage.get_guild_settings(&gid).await?; + let previous_settings = settings.clone(); + + if let Some(channel) = pinboard_channel { + debug!("Setting pinboard_channel to {channel} for {gid}"); + settings.pinboard_channel = Some(channel.id); + } - if let Some(watch) = pinboard_watch { - let channels = split_argument(&watch); - settings.pinboard_watch = (!channels.is_empty()).then_some(channels); - } + if let Some(watch) = pinboard_watch { + let channels = split_argument(&watch); + settings.pinboard_watch = (!channels.is_empty()).then_some(channels); + } - if let Some(enabled) = pinboard_enabled { - debug!("Setting pinboard_enabled to {enabled} for {gid}"); - settings.pinboard_enabled = enabled; - } + if let Some(enabled) = pinboard_enabled { + debug!("Setting pinboard_enabled to {enabled} for {gid}"); + settings.pinboard_enabled = enabled; + } - if let Some(channel) = reactboard_channel { - debug!("Setting reactboard_channel to {channel} for {gid}"); - settings.reactboard_channel = Some(channel.id); - } + if let Some(channel) = reactboard_channel { + debug!("Setting reactboard_channel to {channel} for {gid}"); + settings.reactboard_channel = Some(channel.id); + } - if let Some(requirement) = reactboard_requirement { - debug!("Setting reactboard_requirement to {requirement} for {gid}"); - settings.reactboard_requirement = Some(requirement); - } + if let Some(requirement) = reactboard_requirement { + debug!("Setting reactboard_requirement to {requirement} for {gid}"); + settings.reactboard_requirement = Some(requirement); + } - if let Some(reaction) = reactboard_reaction { - let emojis: Vec<ReactionType> = - reaction.split(',').filter_map(|r| r.parse().ok()).collect(); - debug!("Setting reactboard_reactions to {emojis:#?} for {gid}"); + if let Some(reaction) = reactboard_reaction { + let emojis: Vec<ReactionType> = + reaction.split(',').filter_map(|r| r.parse().ok()).collect(); + debug!("Setting reactboard_reactions to {emojis:#?} for {gid}"); - settings.reactboard_reactions = Some(emojis); - } + settings.reactboard_reactions = Some(emojis); + } - if let Some(enabled) = reactboard_enabled { - debug!("Setting reactboard_enabled to {enabled} for {gid}"); - settings.reactboard_enabled = enabled; - } + if let Some(enabled) = reactboard_enabled { + debug!("Setting reactboard_enabled to {enabled} for {gid}"); + settings.reactboard_enabled = enabled; + } - if let Some(enabled) = optional_commands_enabled { - debug!("Setting optional_commands_enabled to {enabled} for {}", gid); - settings.optional_commands_enabled = enabled; - } + if let Some(enabled) = optional_commands_enabled { + debug!("Setting optional_commands_enabled to {enabled} for {}", gid); + settings.optional_commands_enabled = enabled; + } - if previous_settings == settings { - debug!("Not updating settings key for {gid} since no changes were made"); - ctx.reply("No changes made, so i'm not updating anything") - .await?; + if previous_settings == settings { + debug!("Not updating settings key for {gid} since no changes were made"); + ctx.reply("No changes made, so i'm not updating anything") + .await?; + } else { + debug!("Updating settings key for {gid}"); + storage.create_guild_settings(settings).await?; + ctx.reply("Configuration updated!").await?; + } } else { - debug!("Updating settings key for {gid}"); - storage.create_guild_settings(settings).await?; - ctx.reply("Configuration updated!").await?; + ctx.reply("I have no storage backend right now, so I can't set settings :(") + .await?; } Ok(()) @@ -145,18 +149,22 @@ pub async fn set( pub async fn get( ctx: Context<'_>, #[description = "The setting you want to get"] setting: Properties, -) -> Result<()> { +) -> Result<(), Error> { let gid = &ctx .guild_id() .ok_or_eyre("Failed to get GuildId from context!")?; - let settings = ctx.data().storage.get_guild_settings(gid).await?; - let value = prop_to_val(&setting, &settings); - - let embed = CreateEmbed::new().field(setting.name(), value, false); - let message = CreateReply::default().embed(embed); + if let Some(storage) = &ctx.data().storage { + let settings = storage.get_guild_settings(gid).await?; + let value = prop_to_val(&setting, &settings); - ctx.send(message).await?; + let embed = CreateEmbed::new().field(setting.name(), value, false); + let message = CreateReply::default().embed(embed); + ctx.send(message).await?; + } else { + ctx.reply("I have no storage backend right now, so I can't fetch settings :(") + .await?; + } Ok(()) } diff --git a/src/commands/general/convert.rs b/src/commands/general/convert.rs index 5e14175..4d38eb2 100644 --- a/src/commands/general/convert.rs +++ b/src/commands/general/convert.rs @@ -1,4 +1,4 @@ -use crate::Context; +use crate::{Context, Error}; use bottomify::bottom; use eyre::Result; @@ -9,7 +9,7 @@ use poise::serenity_prelude::constants::MESSAGE_CODE_LIMIT; slash_command, subcommands("to_fahrenheit", "to_celsius", "to_bottom", "from_bottom") )] -pub async fn convert(_ctx: Context<'_>) -> Result<()> { +pub async fn convert(_: Context<'_>) -> Result<(), Error> { Ok(()) } @@ -18,7 +18,7 @@ pub async fn convert(_ctx: Context<'_>) -> Result<()> { pub async fn to_celsius( ctx: Context<'_>, #[description = "What teawie will convert"] degrees_fahrenheit: f32, -) -> Result<()> { +) -> Result<(), Error> { let temp = (degrees_fahrenheit - 32.0) * (5.0 / 9.0); ctx.say(temp.to_string()).await?; Ok(()) @@ -29,7 +29,7 @@ pub async fn to_celsius( pub async fn to_fahrenheit( ctx: Context<'_>, #[description = "What teawie will convert"] degrees_celsius: f32, -) -> Result<()> { +) -> Result<(), Error> { let temp = (degrees_celsius * (9.0 / 5.0)) + 32.0; ctx.say(temp.to_string()).await?; Ok(()) @@ -40,7 +40,7 @@ pub async fn to_fahrenheit( pub async fn to_bottom( ctx: Context<'_>, #[description = "What teawie will translate into bottom"] message: String, -) -> Result<()> { +) -> Result<(), Error> { let encoded = bottom::encode_string(&message); ctx.say(encoded).await?; Ok(()) @@ -51,7 +51,7 @@ pub async fn to_bottom( pub async fn from_bottom( ctx: Context<'_>, #[description = "What teawie will translate from bottom"] message: String, -) -> Result<()> { +) -> Result<(), Error> { let resp: String; if let Ok(decoded) = bottom::decode_string(&message.clone()) { diff --git a/src/commands/general/mod.rs b/src/commands/general/mod.rs index c872272..82af4d2 100644 --- a/src/commands/general/mod.rs +++ b/src/commands/general/mod.rs @@ -1,22 +1,6 @@ -use crate::Data; - -use eyre::Report; -use poise::Command; - -mod ask; -mod bing; -mod config; -mod convert; -mod random; -mod version; - -pub fn to_comands() -> Vec<Command<Data, Report>> { - vec![ - ask::ask(), - bing::bing(), - config::config(), - convert::convert(), - random::random(), - version::version(), - ] -} +pub mod ask; +pub mod bing; +pub mod config; +pub mod convert; +pub mod random; +pub mod version; diff --git a/src/commands/general/random.rs b/src/commands/general/random.rs index 7c7ceff..92e9188 100644 --- a/src/commands/general/random.rs +++ b/src/commands/general/random.rs @@ -1,31 +1,34 @@ -use crate::{api, consts, utils, Context}; +use crate::{api, consts, utils, Context, Error}; -use eyre::Result; - -#[allow(clippy::unused_async)] #[poise::command(slash_command, subcommands("lore", "teawie", "shiggy"))] -pub async fn random(_ctx: Context<'_>) -> Result<()> { +#[allow(clippy::unused_async)] +pub async fn random(_: Context<'_>) -> Result<(), Error> { Ok(()) } /// Get a random piece of teawie lore! #[poise::command(prefix_command, slash_command)] -pub async fn lore(ctx: Context<'_>) -> Result<()> { +pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { let resp = utils::random_choice(consts::LORE)?; ctx.say(resp).await?; + Ok(()) } /// Get a random teawie #[poise::command(prefix_command, slash_command)] -pub async fn teawie(ctx: Context<'_>) -> Result<()> { - let url = api::guzzle::get_random_teawie().await?; - utils::send_url_as_embed(ctx, url).await +pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { + let url = api::guzzle::random_teawie().await?; + utils::send_url_as_embed(ctx, url).await?; + + Ok(()) } /// Get a random shiggy #[poise::command(prefix_command, slash_command)] -pub async fn shiggy(ctx: Context<'_>) -> Result<()> { - let url = api::shiggy::get_random_shiggy().await?; - utils::send_url_as_embed(ctx, url).await +pub async fn shiggy(ctx: Context<'_>) -> Result<(), Error> { + let url = api::shiggy::random_shiggy().await?; + utils::send_url_as_embed(ctx, url).await?; + + Ok(()) } diff --git a/src/commands/general/version.rs b/src/commands/general/version.rs index e392903..5f8eac9 100644 --- a/src/commands/general/version.rs +++ b/src/commands/general/version.rs @@ -1,16 +1,13 @@ -use crate::colors::Colors; -use crate::Context; +use crate::{consts::Colors, Context, Error}; -use eyre::Result; -use poise::serenity_prelude::CreateEmbed; -use poise::CreateReply; +use std::env::consts::{ARCH, OS}; + +use poise::{serenity_prelude::CreateEmbed, CreateReply}; /// Get version info #[poise::command(slash_command)] -pub async fn version(ctx: Context<'_>) -> Result<()> { +pub async fn version(ctx: Context<'_>) -> Result<(), Error> { let sha = option_env!("GIT_SHA").unwrap_or("main"); - let target = option_env!("TARGET").unwrap_or("Unknown"); - let revision_url = format!( "[{}]({}/tree/{})", sha, @@ -18,15 +15,16 @@ pub async fn version(ctx: Context<'_>) -> Result<()> { sha, ); + let os_info = format!("{ARCH}-{OS}"); + let fields = [ ( "Version:", option_env!("CARGO_PKG_VERSION").unwrap_or("not found"), false, ), - ("Target:", target, false), + ("OS:", &os_info, false), ("Revision:", &revision_url, false), - ("User Agent:", &crate::api::USER_AGENT, false), ]; let embed = CreateEmbed::new() diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 88a47b3..e55419b 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,20 +1,53 @@ -use crate::Data; - -use eyre::Report; -use poise::Command; +use crate::{Data, Error}; mod general; mod moderation; mod optional; -pub fn global() -> Vec<Command<Data, Report>> { - general::to_comands() +type Command = poise::Command<Data, Error>; + +#[macro_export] +macro_rules! cmd { + ($module: ident, $name: ident) => { + $module::$name::$name() + }; + + ($module: ident, $name: ident, $func: ident) => { + $module::$name::$func() + }; +} + +pub fn to_vec() -> Vec<Command> { + vec![ + cmd!(general, ask), + cmd!(general, bing), + cmd!(general, config), + cmd!(general, convert), + cmd!(general, random), + cmd!(general, version), + cmd!(moderation, clear_messages), + cmd!(optional, copypasta), + cmd!(optional, teawiespam), + cmd!(optional, uwurandom), + ] } -pub fn optional() -> Vec<Command<Data, Report>> { - optional::to_commands() +pub fn to_vec_global() -> Vec<Command> { + vec![ + cmd!(general, ask), + cmd!(general, bing), + cmd!(general, config), + cmd!(general, convert), + cmd!(general, random), + cmd!(general, version), + cmd!(moderation, clear_messages), + ] } -pub fn moderation() -> Vec<Command<Data, Report>> { - moderation::to_commands() +pub fn to_vec_optional() -> Vec<Command> { + vec![ + cmd!(optional, copypasta), + cmd!(optional, teawiespam), + cmd!(optional, uwurandom), + ] } diff --git a/src/commands/moderation/clear.rs b/src/commands/moderation/clear.rs deleted file mode 100644 index bfc9c38..0000000 --- a/src/commands/moderation/clear.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::Context; - -use eyre::{Context as _, Result}; -use log::debug; -use poise::serenity_prelude::futures::{StreamExt, TryStreamExt}; - -#[poise::command( - slash_command, - ephemeral, - required_permissions = "MANAGE_MESSAGES", - default_member_permissions = "MANAGE_MESSAGES" -)] -pub async fn clear_messages( - ctx: Context<'_>, - #[description = "How many messages to delete"] num_messages: usize, -) -> Result<()> { - ctx.defer_ephemeral().await?; - - let channel = ctx.channel_id(); - let messages = channel - .messages_iter(ctx) - .take(num_messages) - .try_fold(Vec::new(), |mut acc, msg| async move { - acc.push(msg); - Ok(acc) - }) - .await - .wrap_err_with(|| { - format!("Couldn't collect {num_messages} messages from channel {channel}") - })?; - - debug!("Clearing {num_messages} messages from channel {channel}!"); - channel.delete_messages(ctx, messages).await?; - - ctx.reply(format!("Deleted {num_messages} message(s)")) - .await?; - - Ok(()) -} diff --git a/src/commands/moderation/clear_messages.rs b/src/commands/moderation/clear_messages.rs new file mode 100644 index 0000000..8761bcb --- /dev/null +++ b/src/commands/moderation/clear_messages.rs @@ -0,0 +1,30 @@ +use crate::{Context, Error}; + +use log::debug; +use poise::serenity_prelude::GetMessages; + +#[poise::command( + slash_command, + guild_only, + ephemeral, + required_permissions = "MANAGE_MESSAGES", + default_member_permissions = "MANAGE_MESSAGES" +)] +pub async fn clear_messages( + ctx: Context<'_>, + #[description = "How many messages to delete"] num_messages: u8, +) -> Result<(), Error> { + ctx.defer_ephemeral().await?; + + let channel = ctx.channel_id(); + let to_get = GetMessages::new().limit(num_messages); + let messages = channel.messages(ctx, to_get).await?; + + debug!("Clearing {num_messages} messages from channel {channel}!"); + channel.delete_messages(ctx, messages).await?; + + ctx.reply(format!("Deleted {num_messages} message(s)")) + .await?; + + Ok(()) +} diff --git a/src/commands/moderation/mod.rs b/src/commands/moderation/mod.rs index 5a8cd08..ed6a7c6 100644 --- a/src/commands/moderation/mod.rs +++ b/src/commands/moderation/mod.rs @@ -1,10 +1 @@ -use crate::Data; - -use eyre::Report; -use poise::Command; - -mod clear; - -pub fn to_commands() -> Vec<Command<Data, Report>> { - vec![clear::clear_messages()] -} +pub mod clear_messages; diff --git a/src/commands/optional/copypasta.rs b/src/commands/optional/copypasta.rs index 15171f8..06440b1 100644 --- a/src/commands/optional/copypasta.rs +++ b/src/commands/optional/copypasta.rs @@ -1,18 +1,14 @@ -use crate::Context; +use crate::{Context, Error}; -use std::collections::HashMap; - -use eyre::{eyre, OptionExt, Result}; use include_dir::{include_dir, Dir}; use log::debug; -const FILES: Dir = include_dir!("src/copypastas"); +const COPYPASTAS: Dir = include_dir!("src/copypastas"); -#[allow(clippy::upper_case_acronyms)] #[derive(Debug, poise::ChoiceParameter)] -pub enum Copypastas { +pub enum Copypasta { Astral, - DVD, + Dvd, Egrill, HappyMeal, Sus, @@ -20,43 +16,27 @@ pub enum Copypastas { Twitter, } -impl Copypastas { - fn as_str(&self) -> &str { - match self { - Copypastas::Astral => "astral", - Copypastas::DVD => "dvd", - Copypastas::Egrill => "egrill", - Copypastas::HappyMeal => "happymeal", - Copypastas::Sus => "sus", - Copypastas::TickTock => "ticktock", - Copypastas::Twitter => "twitter", - } +impl ToString for Copypasta { + fn to_string(&self) -> String { + let str = match self { + Self::Astral => "astral", + Self::Dvd => "dvd", + Self::Egrill => "egrill", + Self::HappyMeal => "happymeal", + Self::Sus => "sus", + Self::TickTock => "ticktock", + Self::Twitter => "twitter", + }; + str.to_string() } } -fn get_copypasta(name: &Copypastas) -> Result<String> { - let mut files: HashMap<&str, &str> = HashMap::new(); - - for file in FILES.files() { - let name = file - .path() - .file_stem() - .ok_or_else(|| eyre!("Couldn't get file stem from {file:#?}"))? - .to_str() - .ok_or_eyre("Couldn't convert file stem to str!")?; - - let contents = file - .contents_utf8() - .ok_or_eyre("Couldnt get contents from copypasta!")?; - - // refer to files by their name w/o extension - files.insert(name, contents); - } - - if files.contains_key(name.as_str()) { - Ok(files[name.as_str()].to_string()) - } else { - Err(eyre!("Couldnt find copypasta {}!", name.as_str())) +impl Copypasta { + fn contents(&self) -> Option<&str> { + let file_name = format!("{}.txt", self.to_string()); + COPYPASTAS + .get_file(file_name) + .and_then(|file| file.contents_utf8()) } } @@ -64,18 +44,30 @@ fn get_copypasta(name: &Copypastas) -> Result<String> { #[poise::command(slash_command)] pub async fn copypasta( ctx: Context<'_>, - #[description = "the copypasta you want to send"] copypasta: Copypastas, -) -> Result<()> { - let gid = ctx.guild_id().unwrap_or_default(); - let settings = ctx.data().storage.get_guild_settings(&gid).await?; + #[description = "the copypasta you want to send"] copypasta: Copypasta, +) -> Result<(), Error> { + if let Some(guild_id) = ctx.guild_id() { + if let Some(storage) = &ctx.data().storage { + let settings = storage.get_guild_settings(&guild_id).await?; + + if !settings.optional_commands_enabled { + debug!("Not running command in {guild_id} since it's disabled"); + ctx.reply("I'm not allowed to do that here").await?; - if !settings.optional_commands_enabled { - debug!("Exited copypasta command in {gid} since it's disabled"); - ctx.say("I'm not allowed to do that here").await?; - return Ok(()); + return Ok(()); + } + } else { + debug!("Ignoring restrictions on command; no storage backend is attached!"); + } + } else { + debug!("Ignoring restrictions on command; we're not in a guild"); } - ctx.say(get_copypasta(©pasta)?).await?; + if let Some(contents) = copypasta.contents() { + ctx.say(contents).await?; + } else { + ctx.reply("I couldn't find that copypasta :(").await?; + } Ok(()) } diff --git a/src/commands/optional/mod.rs b/src/commands/optional/mod.rs index 39abdcb..95c39bd 100644 --- a/src/commands/optional/mod.rs +++ b/src/commands/optional/mod.rs @@ -1,16 +1,3 @@ -use crate::Data; - -use eyre::Report; -use poise::Command; - -mod copypasta; -mod teawiespam; -mod uwurandom; - -pub fn to_commands() -> Vec<Command<Data, Report>> { - vec![ - copypasta::copypasta(), - teawiespam::teawiespam(), - uwurandom::uwurandom(), - ] -} +pub mod copypasta; +pub mod teawiespam; +pub mod uwurandom; diff --git a/src/commands/optional/teawiespam.rs b/src/commands/optional/teawiespam.rs index 7f7ba79..3a9a387 100644 --- a/src/commands/optional/teawiespam.rs +++ b/src/commands/optional/teawiespam.rs @@ -1,21 +1,29 @@ -use crate::Context; +use crate::{Context, Error}; -use eyre::Result; use log::debug; /// teawie will spam you. #[poise::command(slash_command)] -pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { - let gid = ctx.guild_id().unwrap_or_default(); - let settings = ctx.data().storage.get_guild_settings(&gid).await?; +pub async fn teawiespam(ctx: Context<'_>) -> Result<(), Error> { + if let Some(guild_id) = ctx.guild_id() { + if let Some(storage) = &ctx.data().storage { + let settings = storage.get_guild_settings(&guild_id).await?; - if !settings.optional_commands_enabled { - debug!("Not running teawiespam in {gid} since it's disabled"); - ctx.say("I'm not allowed to do that here").await?; - return Ok(()); + if !settings.optional_commands_enabled { + debug!("Not running command in {guild_id} since it's disabled"); + ctx.say("I'm not allowed to do that here").await?; + + return Ok(()); + } + } else { + debug!("Ignoring restrictions on command; no storage backend is attached!"); + } + } else { + debug!("Ignoring restrictions on command; we're not in a guild."); } let wies = "<:teawiesmile:1056438046440042546>".repeat(50); ctx.say(wies).await?; + Ok(()) } diff --git a/src/commands/optional/uwurandom.rs b/src/commands/optional/uwurandom.rs index 312e54f..e717d5e 100644 --- a/src/commands/optional/uwurandom.rs +++ b/src/commands/optional/uwurandom.rs @@ -1,4 +1,4 @@ -use crate::Context; +use crate::{Context, Error}; use eyre::Result; use log::debug; @@ -12,14 +12,22 @@ pub async fn uwurandom( #[min = 1] #[max = 2000] length: Option<u16>, -) -> Result<()> { - let gid = ctx.guild_id().unwrap_or_default(); - let settings = ctx.data().storage.get_guild_settings(&gid).await?; +) -> Result<(), Error> { + if let Some(guild_id) = ctx.guild_id() { + if let Some(storage) = &ctx.data().storage { + let settings = storage.get_guild_settings(&guild_id).await?; - if !settings.optional_commands_enabled { - debug!("Not running uwurandom in {gid} since it's disabled"); - ctx.say("I'm not allowed to do that here").await?; - return Ok(()); + if !settings.optional_commands_enabled { + debug!("Not running command in {guild_id} since it's disabled"); + ctx.say("I'm not allowed to do that here").await?; + + return Ok(()); + } + } else { + debug!("Ignoring restrictions on command; no storage backend is attached!"); + } + } else { + debug!("Ignoring restrictions on command; we're not in a guild"); } let length = length.unwrap_or(rand::thread_rng().gen_range(1..50)); diff --git a/src/consts.rs b/src/consts.rs index b108f34..0b12334 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,36 @@ +#![allow(clippy::unreadable_literal)] +use std::sync::OnceLock; + +use poise::serenity_prelude::{Colour, Permissions, Scope}; + +pub fn bot_permissions() -> &'static Permissions { + static BOT_PERMISSIONS: OnceLock<Permissions> = OnceLock::new(); + BOT_PERMISSIONS.get_or_init(|| { + Permissions::MANAGE_ROLES + | Permissions::MANAGE_CHANNELS + | Permissions::KICK_MEMBERS + | Permissions::BAN_MEMBERS + | Permissions::MANAGE_NICKNAMES + | Permissions::VIEW_CHANNEL + | Permissions::MODERATE_MEMBERS + | Permissions::SEND_MESSAGES + | Permissions::CREATE_PUBLIC_THREADS + | Permissions::CREATE_PRIVATE_THREADS + | Permissions::SEND_MESSAGES_IN_THREADS + | Permissions::MANAGE_MESSAGES + | Permissions::MANAGE_THREADS + | Permissions::EMBED_LINKS + | Permissions::ATTACH_FILES + | Permissions::READ_MESSAGE_HISTORY + | Permissions::ADD_REACTIONS + }) +} + +pub fn bot_scopes() -> &'static Vec<Scope> { + static BOT_SCOPES: OnceLock<Vec<Scope>> = OnceLock::new(); + BOT_SCOPES.get_or_init(|| vec![Scope::Bot, Scope::ApplicationsCommands]) +} + pub const TEAMOJIS: [&str; 15] = [ "<:teawiecry:1056438041872433303>", "<:teawiederp:1056438043109757018>", @@ -64,3 +97,19 @@ pub const LORE: [&str; 22] = [ "Teawie is friends with BlĂ„haj.", "Consuming Teawie, and other Teawie variants is not recommended, since it will allow Teawie to take over the consumer in the same way as being shot with a Teawie \"bullet\".", ]; + +pub enum Colors { + Blue, + Orange, + Red, +} + +impl From<Colors> for Colour { + fn from(val: Colors) -> Self { + match val { + Colors::Blue => Colour::from(0x88C7FD), + Colors::Orange => Colour::from(0xFFB34A), + Colors::Red => Colour::from(0xFF5E4A), + } + } +} diff --git a/src/copypastas/ticktock.txt b/src/copypastas/ticktock.txt index 9fdd334..bd4f36e 100644 --- a/src/copypastas/ticktock.txt +++ b/src/copypastas/ticktock.txt @@ -1,8 +1,8 @@ -Tick-tock -Heavy like a Brinks truck -Looking like I'm tip-top -Shining like a wristwatch -Time will grab your wrist -Lock it down 'til the thing pop -Can you stick around for a minute 'til the ring stop? +Tick-tock +Heavy like a Brinks truck +Looking like I'm tip-top +Shining like a wristwatch +Time will grab your wrist +Lock it down 'til the thing pop +Can you stick around for a minute 'til the ring stop? Please, God diff --git a/src/handlers/error.rs b/src/handlers/error.rs index afd1241..e706fec 100644 --- a/src/handlers/error.rs +++ b/src/handlers/error.rs @@ -1,12 +1,10 @@ -use crate::colors::Colors; -use crate::Data; +use crate::{consts::Colors, Data, Error}; -use eyre::Report; use log::error; use poise::serenity_prelude::{CreateEmbed, Timestamp}; use poise::{CreateReply, FrameworkError}; -pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { +pub async fn handle(error: poise::FrameworkError<'_, Data, Error>) { match error { FrameworkError::Setup { error, framework, .. @@ -46,7 +44,7 @@ pub async fn handle(error: poise::FrameworkError<'_, Data, Report>) { error => { if let Err(e) = poise::builtins::on_error(error).await { - error!("Unhandled error occured:\n{e:#?}"); + error!("Unhandled error occurred:\n{e:#?}"); } } } diff --git a/src/handlers/event/guild.rs b/src/handlers/event/guild.rs index 51ae3b7..774179c 100644 --- a/src/handlers/event/guild.rs +++ b/src/handlers/event/guild.rs @@ -3,30 +3,35 @@ use log::{debug, warn}; use poise::serenity_prelude::{Guild, UnavailableGuild}; use crate::{storage, Data}; -use storage::Settings; - -pub async fn handle_create(guild: &Guild, _is_new: &bool, data: &Data) -> Result<()> { - let storage = &data.storage; - - if storage.guild_settings_exist(&guild.id).await? { - debug!("Not recreating settings key for {}", guild.id); - return Ok(()); +use storage::settings::Settings; + +pub async fn handle_create(guild: &Guild, data: &Data) -> Result<()> { + if let Some(storage) = &data.storage { + if storage.guild_settings_exist(&guild.id).await? { + debug!("Not recreating settings key for {}", guild.id); + return Ok(()); + } + + let settings = Settings { + guild_id: guild.id, + ..Default::default() + }; + + warn!("Creating new settings key for {}:\n{settings:#?}", guild.id); + storage.create_guild_settings(settings).await?; + } else { + warn!("Can't create guild settings; no storage backend found!"); } - let settings = Settings { - guild_id: guild.id, - optional_commands_enabled: false, - ..Default::default() - }; - - warn!("Creating new settings key for {}:\n{settings:#?}", guild.id); - storage.create_guild_settings(settings).await?; - Ok(()) } pub async fn handle_delete(guild: &UnavailableGuild, data: &Data) -> Result<()> { - data.storage.delete_guild_settings(&guild.id).await?; + if let Some(storage) = &data.storage { + storage.delete_guild_settings(&guild.id).await?; + } else { + warn!("Can't delete guild settings; no storage backend found!"); + } Ok(()) } diff --git a/src/handlers/event/message.rs b/src/handlers/event/message.rs index 3054e27..67dbb21 100644 --- a/src/handlers/event/message.rs +++ b/src/handlers/event/message.rs @@ -1,16 +1,10 @@ use crate::{consts, Data}; -use eyre::{eyre, Report, Result}; -use log::debug; +use eyre::{eyre, Result}; +use log::{debug, warn}; use poise::serenity_prelude::{Context, Message}; -use poise::FrameworkContext; - -pub async fn handle( - ctx: &Context, - _framework: FrameworkContext<'_, Data, Report>, - msg: &Message, - data: &Data, -) -> Result<()> { + +pub async fn handle(ctx: &Context, msg: &Message, data: &Data) -> Result<()> { if should_echo(ctx, msg, data).await? { msg.reply(ctx, &msg.content).await?; } @@ -27,11 +21,16 @@ async fn should_echo(ctx: &Context, msg: &Message, data: &Data) -> Result<bool> let gid = msg .guild_id .ok_or_else(|| eyre!("Couldn't get GuildId from {}!", msg.id))?; - let settings = data.storage.get_guild_settings(&gid).await?; - if !settings.optional_commands_enabled { - debug!("Not echoing in guild {gid}"); - return Ok(false); + if let Some(storage) = &data.storage { + let settings = storage.get_guild_settings(&gid).await?; + + if !settings.optional_commands_enabled { + debug!("Not echoing in guild {gid}"); + return Ok(false); + } + } else { + warn!("Ignoring restrictions on echoing messages; no storage backend is attached!"); } let content = &msg.content; diff --git a/src/handlers/event/mod.rs b/src/handlers/event/mod.rs index 9c986e1..cc7d727 100644 --- a/src/handlers/event/mod.rs +++ b/src/handlers/event/mod.rs @@ -1,9 +1,8 @@ -use crate::Data; +use crate::{consts, Data, Error}; -use eyre::{Report, Result}; -use log::info; -use poise::serenity_prelude as serenity; -use poise::FrameworkContext; +use eyre::Result; +use log::{debug, info}; +use poise::serenity_prelude::{self as serenity, CreateBotAuthParameters}; use serenity::FullEvent; mod guild; @@ -11,19 +10,24 @@ mod message; mod pinboard; mod reactboard; -pub async fn handle( - ctx: &serenity::Context, - event: &FullEvent, - framework: FrameworkContext<'_, Data, Report>, - data: &Data, -) -> Result<()> { +pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<(), Error> { match event { FullEvent::Ready { data_about_bot } => { info!("Logged in as {}!", data_about_bot.user.name); + + if let Ok(invite_link) = CreateBotAuthParameters::new().auto_client_id(ctx).await { + let link = invite_link + .scopes(consts::bot_scopes()) + .permissions(*consts::bot_permissions()) + .build(); + info!("Invite me to your server at {link}"); + } else { + debug!("Not displaying invite_link since we couldn't find our client ID"); + } } FullEvent::Message { new_message } => { - message::handle(ctx, framework, new_message, data).await?; + message::handle(ctx, new_message, data).await?; pinboard::handle(ctx, new_message, data).await?; } @@ -31,8 +35,8 @@ pub async fn handle( reactboard::handle(ctx, add_reaction, data).await?; } - FullEvent::GuildCreate { guild, is_new } => { - guild::handle_create(guild, &is_new.unwrap_or_default(), data).await?; + FullEvent::GuildCreate { guild, is_new: _ } => { + guild::handle_create(guild, data).await?; } FullEvent::GuildDelete { diff --git a/src/handlers/event/pinboard.rs b/src/handlers/event/pinboard.rs index be10eac..5b7d454 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/handlers/event/pinboard.rs @@ -1,7 +1,7 @@ use crate::{utils, Data}; use eyre::{eyre, Context as _, OptionExt as _, Result}; -use log::debug; +use log::{debug, warn}; use poise::serenity_prelude::{ ChannelId, Context, CreateAllowedMentions, CreateMessage, Message, MessageType, User, }; @@ -12,7 +12,12 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> } let gid = message.guild_id.unwrap_or_default(); - let settings = data.storage.get_guild_settings(&gid).await?; + let Some(storage) = &data.storage else { + warn!("Can't create PinBoard entry; no storage backend found!"); + return Ok(()); + }; + + let settings = storage.get_guild_settings(&gid).await?; if !settings.pinboard_enabled { debug!("PinBoard is disabled in {gid}, ignoring"); @@ -53,13 +58,13 @@ pub async fn handle(ctx: &Context, message: &Message, data: &Data) -> Result<()> .find(|pin| pin.id == reference_id) .ok_or_else(|| eyre!("Couldn't find a pin for message {reference_id}!"))?; - redirect(ctx, pin, &message.author, target).await?; + redirect(ctx, pin, &message.author, &target).await?; pin.unpin(ctx).await?; Ok(()) } -async fn redirect(ctx: &Context, pin: &Message, pinner: &User, target: ChannelId) -> Result<()> { +async fn redirect(ctx: &Context, pin: &Message, pinner: &User, target: &ChannelId) -> Result<()> { let embed = utils::resolve_message_to_embed(ctx, pin).await; let mentions = CreateAllowedMentions::new().empty_roles().empty_users(); let message = CreateMessage::default() diff --git a/src/handlers/event/reactboard.rs b/src/handlers/event/reactboard.rs index 4316633..75fc858 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/handlers/event/reactboard.rs @@ -1,13 +1,14 @@ use crate::{storage, utils, Data}; -use storage::ReactBoardEntry; +use storage::reactboard::ReactBoardEntry; use eyre::{eyre, Context as _, Result}; -use log::debug; +use log::{debug, warn}; use poise::serenity_prelude::{ Context, CreateMessage, EditMessage, GuildId, Message, MessageReaction, Reaction, }; pub async fn handle(ctx: &Context, reaction: &Reaction, data: &Data) -> Result<()> { + // TODO @getchoo: don't do anything if this message is old let msg = reaction .message(&ctx.http) .await @@ -45,7 +46,11 @@ async fn send_to_reactboard( guild_id: &GuildId, data: &Data, ) -> Result<()> { - let storage = &data.storage; + let Some(storage) = &data.storage else { + warn!("Can't make ReactBoard entry; no storage backend found!"); + return Ok(()); + }; + let settings = storage.get_guild_settings(guild_id).await?; // make sure everything is in order... @@ -64,7 +69,17 @@ async fn send_to_reactboard( return Ok(()); } - if reaction.count < settings.reactboard_requirement.unwrap_or(5) { + let count = if msg + .reaction_users(ctx, reaction.reaction_type.clone(), None, None) + .await? + .contains(&msg.author) + { + reaction.count - 1 + } else { + reaction.count + }; + + if count < settings.reactboard_requirement.unwrap_or(5) { debug!( "Ignoring message {} on ReactBoard, not enough reactions", msg.id @@ -72,60 +87,57 @@ async fn send_to_reactboard( return Ok(()); } - let content = format!("{} **#{}**", reaction.reaction_type, reaction.count); + let content = format!("{} **#{}**", reaction.reaction_type, count); - // bump reaction count if previous entry exists - if storage.reactboard_entry_exists(guild_id, &msg.id).await? { - let old_entry = storage.get_reactboard_entry(guild_id, &msg.id).await?; + let entry = if storage.reactboard_entry_exists(guild_id, &msg.id).await? { + // bump reaction count if previous entry exists + let mut entry = storage.get_reactboard_entry(guild_id, &msg.id).await?; // bail if we don't need to edit anything - if old_entry.reaction_count >= reaction.count { + if entry.reaction_count >= count { debug!("Message {} doesn't need updating", msg.id); return Ok(()); } debug!( "Bumping {} reaction count from {} to {}", - msg.id, old_entry.reaction_count, reaction.count + msg.id, entry.reaction_count, count ); let edited = EditMessage::new().content(content); ctx.http - .get_message(old_entry.posted_channel_id, old_entry.posted_message_id) + .get_message(entry.posted_channel_id, entry.posted_message_id) .await .wrap_err_with(|| { format!( "Couldn't get previous message from ReactBoardEntry {} in Redis DB!", - old_entry.original_message_id + entry.original_message_id ) })? .edit(ctx, edited) .await?; // update reaction count in redis - let mut new_entry = old_entry.clone(); - new_entry.reaction_count = reaction.count; - - debug!("Updating ReactBoard entry\nOld entry:\n{old_entry:#?}\n\nNew:\n{new_entry:#?}\n",); - storage.create_reactboard_entry(guild_id, new_entry).await?; - // make new message and add entry to redis otherwise + entry.reaction_count = count; + entry } else { + // make new message and add entry to redis otherwise let embed = utils::resolve_message_to_embed(ctx, msg).await; let message = CreateMessage::default().content(content).embed(embed); let resp = target.send_message(ctx, message).await?; - let entry = ReactBoardEntry { + ReactBoardEntry { original_message_id: msg.id, - reaction_count: reaction.count, + reaction_count: count, posted_channel_id: resp.channel_id, posted_message_id: resp.id, - }; + } + }; - debug!("Creating new ReactBoard entry:\n{entry:#?}"); - storage.create_reactboard_entry(guild_id, entry).await?; - } + debug!("Creating new ReactBoard entry:\n{entry:#?}"); + storage.create_reactboard_entry(guild_id, entry).await?; Ok(()) } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index 2ae0539..1610d23 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,5 +1,2 @@ -mod error; -mod event; - -pub use error::handle as handle_error; -pub use event::handle as handle_event; +pub mod error; +pub mod event; diff --git a/src/main.rs b/src/main.rs index 806c7b3..7f19b9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,106 +1,94 @@ -#![warn(clippy::all, clippy::pedantic, clippy::perf)] -#![allow(clippy::missing_errors_doc, clippy::used_underscore_binding)] -#![forbid(unsafe_code)] - -use std::sync::Arc; -use std::time::Duration; - -use eyre::{eyre, Context as _, Report, Result}; -use log::{info, warn}; -use owo_colors::OwoColorize; -use poise::serenity_prelude as serenity; -use poise::{EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions}; -use redis::ConnectionLike; -use storage::Storage; +use std::{sync::Arc, time::Duration}; + +use eyre::{Context as _, Report, Result}; +use log::{info, trace, warn}; +use poise::{ + serenity_prelude::{self 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 colors; mod commands; mod consts; mod handlers; mod storage; mod utils; -type Context<'a> = poise::Context<'a, Data, Report>; +use storage::Storage; + +type Error = Box<dyn std::error::Error + Send + Sync>; +type Context<'a> = poise::Context<'a, Data, Error>; -#[derive(Clone)] +#[derive(Clone, Debug, Default)] pub struct Data { - storage: Storage, + storage: Option<Storage>, } -impl Data { - pub fn new() -> Result<Self> { - let redis_url = - std::env::var("REDIS_URL").wrap_err("Couldn't find Redis URL in environment!")?; - - let storage = Storage::new(&redis_url)?; - - Ok(Self { storage }) - } -} +async fn setup(ctx: &serenity::Context) -> Result<Data, Error> { + let storage = Storage::from_env().ok(); -async fn setup( - ctx: &serenity::Context, - _ready: &serenity::Ready, - framework: &Framework<Data, Report>, -) -> Result<Data> { - let data = Data::new()?; - let mut client = data.storage.client.clone(); - - if !client.check_connection() { - return Err(eyre!( - "Couldn't connect to storage! Is your daemon running?" - )); - } + if let Some(storage) = storage.as_ref() { + if !storage.clone().is_connected() { + return Err( + "You specified a storage backend but there's no connection! Is it running?".into(), + ); + } + trace!("Storage backend connected!"); - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - info!("Registered global commands!"); + poise::builtins::register_globally(ctx, &commands::to_vec_global()).await?; + info!("Registered global commands!"); - // register "extra" commands in guilds that allow it - let guilds = data.storage.get_opted_guilds().await?; + // register "extra" commands in guilds that allow it + let guilds = storage.get_opted_guilds().await?; - for guild in guilds { - poise::builtins::register_in_guild(ctx, &commands::optional(), guild).await?; + for guild in guilds { + poise::builtins::register_in_guild(ctx, &commands::to_vec_optional(), guild).await?; - info!("Registered guild commands to {}", guild); + info!("Registered guild commands to {}", guild); + } + } else { + warn!("No storage backend was specified. Features requiring storage will be disabled"); + warn!("Registering optional commands globally since there's no storage backend"); + poise::builtins::register_globally(ctx, &commands::to_vec()).await?; } + let data = Data { storage }; + Ok(data) } async fn handle_shutdown(shard_manager: Arc<serenity::ShardManager>, reason: &str) { warn!("{reason}! Shutting down bot..."); shard_manager.shutdown_all().await; - println!("{}", "Everything is shutdown. Goodbye!".green()); + println!("Everything is shutdown. Goodbye!"); } #[tokio::main] async fn main() -> Result<()> { - color_eyre::install()?; dotenvy::dotenv().ok(); + color_eyre::install()?; env_logger::init(); - let token = std::env::var("TOKEN").wrap_err_with(|| "Couldn't find token in environment!")?; + let token = std::env::var("TOKEN").wrap_err("Couldn't find bot token in environment!")?; let intents = serenity::GatewayIntents::non_privileged() | serenity::GatewayIntents::MESSAGE_CONTENT; let options = FrameworkOptions { - commands: { - let mut commands = commands::global(); - commands.append(&mut commands::moderation()); - commands - }, - on_error: |error| Box::pin(handlers::handle_error(error)), + commands: commands::to_vec(), + on_error: |error| Box::pin(handlers::error::handle(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)) + event_handler: |ctx, event, _framework, data| { + Box::pin(handlers::event::handle(ctx, event, data)) }, prefix_options: PrefixFrameworkOptions { @@ -116,7 +104,7 @@ async fn main() -> Result<()> { let framework = Framework::builder() .options(options) - .setup(|ctx, ready, framework| Box::pin(setup(ctx, ready, framework))) + .setup(|ctx, _ready, _framework| Box::pin(setup(ctx))) .build(); let mut client = serenity::ClientBuilder::new(token, intents) @@ -124,12 +112,15 @@ async fn main() -> Result<()> { .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(Report::from), _ = sigterm.recv() => { - handle_shutdown(shard_manager, "Recieved SIGTERM").await; + handle_shutdown(shard_manager, "Received SIGTERM").await; std::process::exit(0); }, _ = ctrl_c() => { diff --git a/src/storage/mod.rs b/src/storage/mod.rs index 0e2f445..45c150a 100644 --- a/src/storage/mod.rs +++ b/src/storage/mod.rs @@ -1,138 +1,47 @@ -use std::fmt::Debug; +use std::{env, fmt::Debug, str::FromStr}; use eyre::Result; use log::debug; use poise::serenity_prelude::{GuildId, MessageId}; -use redis::{AsyncCommands, Client, FromRedisValue, ToRedisArgs}; +use redis::{AsyncCommands, Client, ConnectionLike, RedisError}; -mod reactboard; -mod settings; -// these are purposefully private. see the comment below -use reactboard::REACTBOARD_KEY; -use settings::SETTINGS_KEY; +pub mod reactboard; +pub mod settings; -pub use reactboard::ReactBoardEntry; -pub use settings::{Properties, Settings}; +use reactboard::ReactBoardEntry; +use settings::Settings; + +pub const REACTBOARD_KEY: &str = "reactboard-v2"; +pub const SETTINGS_KEY: &str = "settings-v1"; #[derive(Clone, Debug)] pub struct Storage { - pub client: Client, + client: Client, } impl Storage { - pub fn new(redis_url: &str) -> Result<Self> { - let client = Client::open(redis_url)?; - - Ok(Self { client }) - } - - /* - these are mainly light abstractions to avoid the `let mut con` - boilerplate, as well as not require the caller to format the - strings for keys - */ - - async fn get_key<T>(&self, key: &str) -> Result<T> - where - T: FromRedisValue, - { - debug!("Getting key {key}"); - - let mut con = self.client.get_async_connection().await?; - let res: T = con.get(key).await?; - - Ok(res) - } - - async fn set_key<'a>( - &self, - key: &str, - value: impl ToRedisArgs + Debug + Send + Sync + 'a, - ) -> Result<()> { - debug!("Creating key {key}:\n{value:#?}"); - - let mut con = self.client.get_async_connection().await?; - con.set(key, value).await?; - - Ok(()) - } - - async fn key_exists(&self, key: &str) -> Result<bool> { - debug!("Checking if key {key} exists"); - - let mut con = self.client.get_async_connection().await?; - let exists: u64 = con.exists(key).await?; - - Ok(exists > 0) - } - - async fn delete_key(&self, key: &str) -> Result<()> { - debug!("Deleting key {key}"); - - let mut con = self.client.get_async_connection().await?; - con.del(key).await?; - - Ok(()) - } - - async fn expire_key(&self, key: &str, expire_seconds: i64) -> Result<()> { - debug!("Expiring key {key} in {expire_seconds}"); - - let mut con = self.client.get_async_connection().await?; - con.expire(key, expire_seconds).await?; - - Ok(()) + pub fn new(client: Client) -> Self { + Self { client } } - async fn add_to_index<'a>( - &self, - key: &str, - member: impl ToRedisArgs + Debug + Send + Sync + 'a, - ) -> Result<()> { - let key = format!("{key}:index"); - debug!("Adding member {member:#?} to index {key}"); - - let mut con = self.client.get_async_connection().await?; - con.sadd(key, member).await?; + pub fn from_env() -> Result<Self> { + let redis_url = env::var("REDIS_URL")?; - Ok(()) + Ok(Self::from_str(&redis_url)?) } - async fn get_index<T>(&self, key: &str) -> Result<Vec<T>> - where - T: FromRedisValue, - { - let key = format!("{key}:index"); - debug!("Getting index {key}"); - - let mut con = self.client.get_async_connection().await?; - let members = con.smembers(key).await?; - - Ok(members) + pub fn is_connected(&mut self) -> bool { + self.client.check_connection() } - async fn delete_from_index<'a>( - &self, - key: &str, - member: impl ToRedisArgs + Debug + Send + Sync + 'a, - ) -> Result<()> { - let key = format!("{key}:index"); - debug!("Removing {member:#?} from index {key}"); - - let mut con = self.client.get_async_connection().await?; - con.srem(key, member).await?; - - Ok(()) - } - - // guild settings - pub async fn create_guild_settings(&self, settings: Settings) -> Result<()> { - let key = format!("{SETTINGS_KEY}:{}", settings.guild_id); + let guild_key = format!("{SETTINGS_KEY}:{}", settings.guild_id); - self.set_key(&key, &settings).await?; - // adding to index since we need to look all of these up sometimes - self.add_to_index(SETTINGS_KEY, u64::from(settings.guild_id)) + let mut con = self.client.get_multiplexed_async_connection().await?; + redis::pipe() + .set(&guild_key, &settings) + .sadd(SETTINGS_KEY, u64::from(settings.guild_id)) + .query_async(&mut con) .await?; Ok(()) @@ -140,32 +49,43 @@ impl Storage { pub async fn get_guild_settings(&self, guild_id: &GuildId) -> Result<Settings> { debug!("Fetching guild settings for {guild_id}"); + let guild_key = format!("{SETTINGS_KEY}:{guild_id}"); - let key = format!("{SETTINGS_KEY}:{guild_id}"); - let settings: Settings = self.get_key(&key).await?; + let mut con = self.client.get_multiplexed_async_connection().await?; + let settings: Settings = con.get(&guild_key).await?; Ok(settings) } pub async fn delete_guild_settings(&self, guild_id: &GuildId) -> Result<()> { - let key = format!("{SETTINGS_KEY}:{guild_id}"); - - self.delete_key(&key).await?; - self.delete_from_index(SETTINGS_KEY, u64::from(*guild_id)) + debug!("Deleting guild settings for {guild_id}"); + let guild_key = format!("{SETTINGS_KEY}:{guild_id}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + redis::pipe() + .del(&guild_key) + .srem(SETTINGS_KEY, u64::from(*guild_id)) + .query_async(&mut con) .await?; Ok(()) } pub async fn guild_settings_exist(&self, guild_id: &GuildId) -> Result<bool> { - let key = format!("{SETTINGS_KEY}:{guild_id}"); - self.key_exists(&key).await + debug!("Checking if guild settings for {guild_id} exist"); + let guild_key = format!("{SETTINGS_KEY}:{guild_id}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + let exists = con.exists(&guild_key).await?; + + Ok(exists) } pub async fn get_all_guild_settings(&self) -> Result<Vec<Settings>> { debug!("Fetching all guild settings"); - let found: Vec<u64> = self.get_index(SETTINGS_KEY).await?; + let mut con = self.client.get_multiplexed_async_connection().await?; + let found: Vec<u64> = con.smembers(SETTINGS_KEY).await?; let mut guilds = vec![]; for key in found { @@ -196,10 +116,14 @@ impl Storage { guild_id: &GuildId, entry: ReactBoardEntry, ) -> Result<()> { - let key = format!("{REACTBOARD_KEY}:{guild_id}:{}", entry.original_message_id); + debug!( + "Creating reactboard entry for {} in {guild_id}", + &entry.original_message_id + ); + let entry_key = format!("{REACTBOARD_KEY}:{guild_id}:{}", entry.original_message_id); - self.set_key(&key, &entry).await?; - self.expire_key(&key, 30 * 24 * 60 * 60).await?; // 30 days + let mut con = self.client.get_multiplexed_async_connection().await?; + con.set_ex(&entry_key, &entry, 30 * 24 * 60 * 60).await?; // 30 days Ok(()) } @@ -209,10 +133,11 @@ impl Storage { guild_id: &GuildId, message_id: &MessageId, ) -> Result<ReactBoardEntry> { - debug!("Fetching reactboard entry in {guild_id}"); + debug!("Fetching reactboard entry {message_id} in {guild_id}"); + let entry_key = format!("{REACTBOARD_KEY}:{guild_id}:{message_id}"); - let key = format!("{REACTBOARD_KEY}:{guild_id}:{message_id}"); - let entry: ReactBoardEntry = self.get_key(&key).await?; + let mut con = self.client.get_multiplexed_async_connection().await?; + let entry: ReactBoardEntry = con.get(&entry_key).await?; Ok(entry) } @@ -222,7 +147,20 @@ impl Storage { guild_id: &GuildId, message_id: &MessageId, ) -> Result<bool> { - let key = format!("{REACTBOARD_KEY}:{guild_id}:{message_id}"); - self.key_exists(&key).await + debug!("Checking if reactboard entry {message_id} exists in {guild_id}"); + let entry_key = format!("{REACTBOARD_KEY}:{guild_id}:{message_id}"); + + let mut con = self.client.get_multiplexed_async_connection().await?; + let exists = con.exists(&entry_key).await?; + + Ok(exists) + } +} + +impl FromStr for Storage { + type Err = RedisError; + fn from_str(s: &str) -> Result<Self, Self::Err> { + let client = Client::open(s)?; + Ok(Self::new(client)) } } diff --git a/src/storage/reactboard.rs b/src/storage/reactboard.rs index 19453df..496739c 100644 --- a/src/storage/reactboard.rs +++ b/src/storage/reactboard.rs @@ -2,8 +2,6 @@ use poise::serenity_prelude::{ChannelId, MessageId}; use redis_macros::{FromRedisValue, ToRedisArgs}; use serde::{Deserialize, Serialize}; -pub const REACTBOARD_KEY: &str = "reactboard-v2"; - #[derive(Clone, Debug, Serialize, Deserialize, FromRedisValue, ToRedisArgs)] pub struct ReactBoardEntry { pub original_message_id: MessageId, diff --git a/src/storage/settings.rs b/src/storage/settings.rs index 76eacc7..cfeda64 100644 --- a/src/storage/settings.rs +++ b/src/storage/settings.rs @@ -2,8 +2,6 @@ use poise::serenity_prelude::{ChannelId, GuildId, ReactionType}; use redis_macros::{FromRedisValue, ToRedisArgs}; use serde::{Deserialize, Serialize}; -pub const SETTINGS_KEY: &str = "settings-v1"; - #[derive(poise::ChoiceParameter)] pub enum Properties { GuildId, @@ -21,12 +19,12 @@ pub enum Properties { pub struct Settings { pub guild_id: GuildId, pub pinboard_channel: Option<ChannelId>, - pub pinboard_watch: Option<Vec<ChannelId>>, pub pinboard_enabled: bool, + pub pinboard_watch: Option<Vec<ChannelId>>, pub reactboard_channel: Option<ChannelId>, + pub reactboard_enabled: bool, pub reactboard_requirement: Option<u64>, pub reactboard_reactions: Option<Vec<ReactionType>>, - pub reactboard_enabled: bool, pub optional_commands_enabled: bool, } diff --git a/src/utils.rs b/src/utils.rs index 1f8e4a7..9b642a7 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{colors, Context}; +use crate::{consts::Colors, Context}; use color_eyre::eyre::{eyre, Result}; use poise::serenity_prelude::{self as serenity, CreateEmbedAuthor, CreateEmbedFooter}; @@ -49,7 +49,7 @@ pub async fn send_url_as_embed(ctx: Context<'_>, url: String) -> Result<()> { .title(title) .image(&url) .url(url) - .color(colors::Colors::Blue); + .color(Colors::Blue); let reply = CreateReply::default().embed(embed); ctx.send(reply).await?; |
