diff options
| author | seth <[email protected]> | 2024-08-09 23:35:41 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-08-09 23:35:41 -0400 |
| commit | b643a6a235b0c1c9902b97421f24eff2b0d0a5ac (patch) | |
| tree | 350794c0e9330fb77367838313bc6bb97278a0aa | |
| parent | 372780546b508684839916e5ad54c9e90456a94f (diff) | |
tree-wide: end of summer cleanup (#214)
* api: refactor & rename module to http
* client: split from main.rs
* tree-wide: use eyre::Report as error
* nix: alejandra -> nixfmt
* nix: start using treefmt-nix
* nix: simplify flake
* nix: refactor derivation & docker image
* nix: remove overlay
* ci: update & cleanup workflows
* commands: assign all commands automatically
* commands/copypasta: remove
* http/teawie: update response struct for upstream rust rewrite
* handlers: rename modules to events; flatten
* crates: rename self to teawie-bot
* nix: fenix -> rust-overlay
i want a specific rust version grrrrrrr
* ci: pin rust to 1.79
this is what our nix dev shell uses and what we can compile on. it seems
the time crate doesn't like v1.80 of the compiler :(
* ci: always run release gates
* nix: fix static toolchain
* nix: rust-overlay -> nixpkgs
* ci: adopt actions-rust-lang actions
* nix: use docker arch names for containers
* crates/time: 0.3.30 -> 0.3.36
fixes building on rust 1.80.0
53 files changed, 577 insertions, 691 deletions
diff --git a/.github/workflows/autobot.yaml b/.github/workflows/autobot.yaml index b57ea58..4e74d0a 100644 --- a/.github/workflows/autobot.yaml +++ b/.github/workflows/autobot.yaml @@ -14,7 +14,8 @@ jobs: pull-requests: write steps: - - uses: dependabot/fetch-metadata@v2 + - name: Fetch metadata + uses: dependabot/fetch-metadata@v2 id: metadata with: github-token: ${{ github.token }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9394567..f084a2a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -3,7 +3,17 @@ name: CI on: push: branches: [main] + paths: + - "**.nix" + - "**.rs" + - "**.lock" + - "Cargo.toml" pull_request: + paths: + - "**.nix" + - "**.rs" + - "**.lock" + - "Cargo.toml" workflow_dispatch: jobs: @@ -13,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [macos-latest, windows-latest] runs-on: ${{ matrix.os }} @@ -22,18 +32,14 @@ jobs: uses: actions/checkout@v4 - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + uses: actions-rust-lang/setup-rust-toolchain@v1 - name: Run build - run: cargo build --locked --release + run: | + cargo build --locked --release - format-and-lint: - name: Format & lint + nix: + name: Nix runs-on: ubuntu-latest @@ -42,21 +48,41 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v12 + uses: DeterminateSystems/nix-installer-action@v13 - name: Setup Nix cache uses: DeterminateSystems/magic-nix-cache-action@v7 - - name: Run treefmt + - name: Run flake checks run: | - nix flake check --all-systems --print-build-logs --show-trace + nix build --print-build-logs --show-trace + + rustfmt: + name: Rustfmt + + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: rustfmt + + - name: Run rustfmt + uses: actions-rust-lang/rustfmt@v1 release-gate: - name: CI Release Gate - needs: [build, format-and-lint] + name: CI Release gate + needs: [build, rustfmt, nix] + + if: ${{ always() }} runs-on: ubuntu-latest steps: - - name: Exit with result - run: echo "We're good to go!" + - name: Exit with error + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 diff --git a/.github/workflows/clippy.yaml b/.github/workflows/clippy.yaml index 56b39df..cd99256 100644 --- a/.github/workflows/clippy.yaml +++ b/.github/workflows/clippy.yaml @@ -2,6 +2,7 @@ name: Clippy on: push: + branches: [main] paths: - 'Cargo.toml' - 'Cargo.lock' @@ -26,31 +27,23 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v12 - - - name: Setup Nix cache - uses: DeterminateSystems/magic-nix-cache-action@v7 - - - name: Setup Rust cache - uses: Swatinem/rust-cache@v2 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + components: clippy - name: Install SARIF tools run: | - nix profile install \ - --inputs-from . \ - github:getchoo/nix-exprs#{clippy-sarif,sarif-fmt} + cargo install clippy-sarif sarif-fmt - name: Fetch Cargo deps run: | - nix develop .#ci --command \ - cargo fetch --locked + cargo fetch --locked - name: Run Clippy continue-on-error: true run: | - nix develop .#ci --command \ - cargo clippy \ + cargo clippy \ --all-features \ --all-targets \ --message-format=json \ diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 30c84d9..0bb28ad 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -3,7 +3,17 @@ name: Docker on: push: branches: [main] + paths: + - "**.nix" + - "**.rs" + - "**.lock" + - "Cargo.toml" pull_request: + paths: + - "**.nix" + - "**.rs" + - "**.lock" + - "Cargo.toml" workflow_dispatch: jobs: @@ -13,7 +23,7 @@ jobs: strategy: fail-fast: false matrix: - arch: [x86_64, aarch64] + arch: [amd64, arm64] runs-on: ubuntu-latest @@ -22,7 +32,7 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v12 + uses: DeterminateSystems/nix-installer-action@v13 - name: Setup Nix cache uses: DeterminateSystems/magic-nix-cache-action@v7 @@ -53,17 +63,21 @@ jobs: name: Docker Release Gate needs: build + if: always() + runs-on: ubuntu-latest steps: - - name: Exit with result - run: echo "We're good to go!" + - name: Exit with error + if: contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') + run: exit 1 push: name: Push image - if: github.event_name == 'push' needs: release-gate + if: github.event_name == 'push' + runs-on: ubuntu-latest permissions: @@ -72,12 +86,9 @@ jobs: env: REGISTRY: ghcr.io USERNAME: ${{ github.actor }} + IMAGE_NAME: teawie-bot steps: - - name: Set image name - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> "$GITHUB_ENV" - - name: Checkout repository uses: actions/checkout@v4 @@ -97,15 +108,15 @@ jobs: env: TAG: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest run: | - architectures=("x86_64" "aarch64") + architectures=("amd64" "arm64") for arch in "${architectures[@]}"; do docker load < images/container-"$arch"/*.tar.gz - docker tag teawiebot:latest-"$arch" "$TAG"-"$arch" + docker tag teawie-bot:latest-"$arch" "$TAG"-"$arch" docker push "$TAG"-"$arch" done docker manifest create "$TAG" \ - --amend "$TAG"-x86_64 \ - --amend "$TAG"-aarch64 + --amend "$TAG"-amd64 \ + --amend "$TAG"-arm64 docker manifest push "$TAG" diff --git a/.github/workflows/update-flake.yaml b/.github/workflows/update-flake.yaml index 968fc15..06eabfd 100644 --- a/.github/workflows/update-flake.yaml +++ b/.github/workflows/update-flake.yaml @@ -20,15 +20,14 @@ jobs: uses: actions/checkout@v4 - name: Install Nix - uses: DeterminateSystems/nix-installer-action@v12 + uses: DeterminateSystems/nix-installer-action@v13 - name: Update flake.lock & make PR - uses: DeterminateSystems/update-flake-lock@v23 id: update + uses: DeterminateSystems/update-flake-lock@v23 with: commit-msg: "nix: update flake.lock" pr-title: "nix: update flake.lock" - token: ${{ secrets.MERGE_TOKEN }} - name: Enable auto-merge if: env.PR_ID != '' @@ -983,25 +983,6 @@ dependencies = [ ] [[package]] -name = "include_dir" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" -dependencies = [ - "include_dir_macros", -] - -[[package]] -name = "include_dir_macros" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] name = "indenter" version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1130,6 +1111,12 @@ dependencies = [ ] [[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2117,7 +2104,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" [[package]] -name = "teawiebot" +name = "teawie-bot" version = "1.0.0" dependencies = [ "bottomify", @@ -2125,7 +2112,6 @@ dependencies = [ "dotenvy", "env_logger", "eyre", - "include_dir", "log", "poise", "rand 0.8.5", @@ -2193,12 +2179,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", + "num-conv", "powerfmt", "serde", "time-core", @@ -2213,10 +2200,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ + "num-conv", "time-core", ] @@ -1,10 +1,8 @@ [package] -name = "teawiebot" +name = "teawie-bot" version = "1.0.0" edition = "2021" repository = "https://github.com/getchoo/teawieBot" -license = "MIT" -readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -17,7 +15,6 @@ eyre = { version = "0.6.12", default-features = false, features = [ "auto-install", "track-caller", ] } -include_dir = "0.7.4" log = "0.4.22" poise = "0.6.1" rand = "0.8.5" @@ -7,14 +7,10 @@ and now in rust!!!)๐๐ ## features / commands -(some are slash, some are not) - **!ask** | ask the bot a question with predefined answers (this is also a slash command, use with /ask) **!teawiespam** | spams :teawiesmile: -**/copypasta** | sends a random copypasta from src\teawie_bot\copypastas - **/random_teawie** | sends out a random teawie, which is a soft cute character made by SympathyTea @@ -1,26 +1,5 @@ { "nodes": { - "fenix": { - "inputs": { - "nixpkgs": [ - "nixpkgs" - ], - "rust-analyzer-src": "rust-analyzer-src" - }, - "locked": { - "lastModified": 1721975407, - "narHash": "sha256-XkNqglPxkfWOkysN5C1aZeoHCozwyRq3nC4jL0IFqlA=", - "owner": "nix-community", - "repo": "fenix", - "rev": "27128b6e467ced6142264d02a884fde45931e708", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1721970117, @@ -39,24 +18,27 @@ }, "root": { "inputs": { - "fenix": "fenix", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" } }, - "rust-analyzer-src": { - "flake": false, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, "locked": { - "lastModified": 1721909430, - "narHash": "sha256-u3e38jvjbxbbYH3caQFPE7gnNFNBjbkqkHRKBx1AOJs=", - "owner": "rust-lang", - "repo": "rust-analyzer", - "rev": "c02a4a31eada45e591b5edb8c1a813ea7b9d408f", + "lastModified": 1721769617, + "narHash": "sha256-6Pqa0bi5nV74IZcENKYRToRNM5obo1EQ+3ihtunJ014=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "8db8970be1fb8be9c845af7ebec53b699fe7e009", "type": "github" }, "original": { - "owner": "rust-lang", - "ref": "nightly", - "repo": "rust-analyzer", + "owner": "numtide", + "repo": "treefmt-nix", "type": "github" } } @@ -3,122 +3,105 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - fenix = { - url = "github:nix-community/fenix"; + + ## Everything below this is optional + ## `inputs.<name>.follows = ""` + + treefmt-nix = { + url = "github:numtide/treefmt-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; }; - outputs = { - self, - nixpkgs, - ... - } @ inputs: let - systems = [ - "x86_64-linux" - "aarch64-linux" - "x86_64-darwin" - "aarch64-darwin" - ]; - - forAllSystems = fn: nixpkgs.lib.genAttrs systems (system: fn nixpkgs.legacyPackages.${system}); - in { - checks = forAllSystems ({ - lib, - pkgs, - ... - }: { - actionlint = pkgs.runCommand "check-actionlint" {} '' - ${lib.getExe pkgs.actionlint} ${./.github/workflows}/* - touch $out - ''; - - deadnix = pkgs.runCommand "check-deadnix" {} '' - ${lib.getExe pkgs.deadnix} --fail ${./.} - touch $out - ''; - - editorconfig = pkgs.runCommand "check-editorconfig" {} '' - cd ${./.} - ${lib.getExe pkgs.editorconfig-checker} \ - -exclude '.git' . - - touch $out - ''; - - rustfmt = pkgs.runCommand "check-rustfmt" {nativeBuildInputs = [pkgs.cargo pkgs.rustfmt];} '' - cd ${./.} - cargo fmt -- --check - touch $out - ''; - - statix = pkgs.runCommand "check-statix" {} '' - ${lib.getExe pkgs.statix} check ${./.} - touch $out - ''; - }); - - devShells = forAllSystems ({ - pkgs, - system, - ... - }: { - 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.${system} - - pkgs.redis - ]; - - inputsFrom = [self.packages.${system}.teawiebot]; - RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; - }; - - ci = pkgs.mkShell { - packages = [ - pkgs.clippy - pkgs.rustfmt - - self.formatter.${system} - ]; - - inputsFrom = [self.packages.${system}.teawiebot]; - }; - }); - - formatter = forAllSystems (pkgs: pkgs.alejandra); - - nixosModules.default = import ./nix/module.nix self; - - packages = forAllSystems ({ - pkgs, - system, - ... - }: let - crossBuildsFor = arch: import ./nix/docker.nix {inherit pkgs arch inputs;}; + outputs = + { + self, + nixpkgs, + treefmt-nix, + }: + let + inherit (nixpkgs) lib; + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = lib.genAttrs systems; + nixpkgsFor = forAllSystems (system: nixpkgs.legacyPackages.${system}); + treefmtFor = forAllSystems (system: treefmt-nix.lib.evalModule nixpkgsFor.${system} ./treefmt.nix); in - { - teawiebot = pkgs.callPackage ./nix/derivation.nix {inherit self;}; - - default = self.packages.${system}.teawiebot; - } - // crossBuildsFor "x86_64" - // crossBuildsFor "aarch64"); - - overlays.default = _: prev: { - teawiebot = prev.callPackage ./nix/derivation.nix {inherit self;}; + { + checks = forAllSystems (system: { + treefmt = treefmtFor.${system}.config.build.check self; + }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + packages = [ + # rust tools + pkgs.clippy + pkgs.rustfmt + pkgs.rust-analyzer + + # nix tools + pkgs.nil + pkgs.statix + + # misc formatter/linters + pkgs.actionlint + self.formatter.${system} + + pkgs.redis + ]; + + inputsFrom = [ self.packages.${system}.teawie-bot ]; + RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; + }; + + ci = pkgs.mkShell { + packages = [ + pkgs.clippy + pkgs.rustfmt + + self.formatter.${system} + ]; + + inputsFrom = [ self.packages.${system}.teawie-bot ]; + }; + } + ); + + formatter = forAllSystems (system: nixpkgsFor.${system}.nixfmt-rfc-style); + + nixosModules.default = import ./nix/module.nix self; + + packages = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + packages' = self.packages.${system}; + + staticWith = pkgs.callPackage ./nix/static.nix { inherit (packages') teawie-bot; }; + containerize = pkgs.callPackage ./nix/containerize.nix { }; + in + { + container-amd64 = containerize packages'.static-x86_64; + container-arm64 = containerize packages'.static-aarch64; + + static-x86_64 = staticWith { arch = "x86_64"; }; + static-aarch64 = staticWith { arch = "aarch64"; }; + + teawie-bot = pkgs.callPackage ./nix/derivation.nix { inherit self; }; + + default = self.packages.${system}.teawie-bot; + } + ); }; - }; } diff --git a/nix/containerize.nix b/nix/containerize.nix new file mode 100644 index 0000000..8175e17 --- /dev/null +++ b/nix/containerize.nix @@ -0,0 +1,17 @@ +{ lib, dockerTools }: +let + containerize = + teawie-bot: + let + inherit (teawie-bot.passthru) crossPkgs; + architecture = crossPkgs.go.GOARCH; + in + dockerTools.buildLayeredImage { + name = "teawie-bot"; + tag = "latest-${architecture}"; + contents = [ dockerTools.caCertificates ]; + config.Cmd = [ (lib.getExe teawie-bot) ]; + inherit architecture; + }; +in +containerize diff --git a/nix/derivation.nix b/nix/derivation.nix index 6a25976..955f601 100644 --- a/nix/derivation.nix +++ b/nix/derivation.nix @@ -3,46 +3,51 @@ stdenv, rustPlatform, darwin, - self ? {inherit ((lib.importTOML ../Cargo.toml).package) version;}, + self ? { }, lto ? true, optimizeSize ? false, }: +let + fs = lib.fileset; +in rustPlatform.buildRustPackage { - pname = "teawiebot"; - version = - (lib.importTOML ../Cargo.toml).package.version - + "-" - + self.shortRev or self.dirtyShortRev or self.version or "unknown"; + pname = "teawie-bot"; + version = (lib.importTOML ../Cargo.toml).package.version or "unknown"; - __structuredAttrs = true; - - src = lib.fileset.toSource { + src = fs.toSource { root = ../.; - fileset = lib.fileset.unions [ - ../src - ../Cargo.toml - ../Cargo.lock - ]; + fileset = fs.intersection (fs.gitTracked ../.) ( + lib.fileset.unions [ + ../src + ../Cargo.toml + ../Cargo.lock + ] + ); }; cargoLock = { lockFile = ../Cargo.lock; }; - buildInputs = lib.optionals stdenv.isDarwin (with darwin.apple_sdk.frameworks; [ - CoreFoundation - Security - SystemConfiguration - darwin.libiconv - ]); + buildInputs = lib.optionals stdenv.isDarwin ( + with darwin.apple_sdk.frameworks; + [ + CoreFoundation + Security + SystemConfiguration + darwin.libiconv + ] + ); - env = let - toRustFlags = lib.mapAttrs' ( - name: - lib.nameValuePair - "CARGO_BUILD_RELEASE_${lib.toUpper (builtins.replaceStrings ["-"] ["_"] name)}" - ); - in + 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"; } @@ -56,11 +61,11 @@ rustPlatform.buildRustPackage { strip = "symbols"; }); - meta = with lib; { - mainProgram = "teawiebot"; + meta = { description = "funni bot"; homepage = "https://github.com/getchoo/teawiebot"; - license = licenses.mit; - maintainers = with maintainers; [getchoo]; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ getchoo ]; + mainProgram = "teawie-bot"; }; } diff --git a/nix/docker.nix b/nix/docker.nix deleted file mode 100644 index 79f49cc..0000000 --- a/nix/docker.nix +++ /dev/null @@ -1,43 +0,0 @@ -{ - pkgs, - arch, - inputs, -}: let - inherit (pkgs) lib; - inputs' = lib.mapAttrs (_: lib.mapAttrs (_: v: v.${pkgs.system} or v)) inputs; - - 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; -in { - "teawiebot-static-${arch}" = inputs'.self.packages.teawiebot.override { - rustPlatform = crossPlatforms.${arch}; - optimizeSize = true; - }; - - "container-${arch}" = pkgs.dockerTools.buildLayeredImage { - name = "teawiebot"; - tag = "latest-${arch}"; - contents = [pkgs.dockerTools.caCertificates]; - config.Cmd = [ - (lib.getExe inputs'.self.packages."teawiebot-static-${arch}") - ]; - - architecture = inputs.nixpkgs.legacyPackages."${arch}-linux".pkgsStatic.go.GOARCH; - }; -} diff --git a/nix/module.nix b/nix/module.nix index c129e68..4e3b683 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -1,14 +1,15 @@ -self: { +self: +{ config, lib, pkgs, ... -}: let +}: +let cfg = config.services.teawiebot; defaultUser = "teawiebot"; - inherit - (lib) + inherit (lib) getExe literalExpression mdDoc @@ -21,18 +22,18 @@ self: { ; inherit (pkgs.stdenv.hostPlatform) system; -in { +in +{ options.services.teawiebot = { - enable = mkEnableOption "teawiebot"; - package = mkPackageOption ( - self.packages.${system} or (builtins.throw "${system} is not supported!") - ) "teawiebot" {}; + enable = mkEnableOption "teawieBot"; + package = mkPackageOption (self.packages.${system} or (builtins.throw "${system} is not supported!") + ) "teawie-bot" { }; 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. + the user will be created, with the specified group as the primary + group. ''; type = types.str; default = defaultUser; @@ -88,10 +89,8 @@ in { systemd.services."teawiebot" = { enable = true; - wantedBy = ["multi-user.target"]; - after = - ["network.target"] - ++ optionals (cfg.redisUrl == "local") ["redis-teawiebot.service"]; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ] ++ optionals (cfg.redisUrl == "local") [ "redis-teawiebot.service" ]; script = '' ${getExe cfg.package} @@ -99,9 +98,10 @@ in { environment = { REDIS_URL = - if cfg.redisUrl == "local" - then "unix:${config.services.redis.servers.teawiebot.unixSocket}" - else cfg.redisUrl; + if cfg.redisUrl == "local" then + "unix:${config.services.redis.servers.teawiebot.unixSocket}" + else + cfg.redisUrl; }; serviceConfig = { @@ -140,9 +140,7 @@ in { }; }; - groups = mkIf (cfg.group == defaultUser) { - ${defaultUser} = {}; - }; + groups = mkIf (cfg.group == defaultUser) { ${defaultUser} = { }; }; }; }; } diff --git a/nix/static.nix b/nix/static.nix new file mode 100644 index 0000000..5a5606f --- /dev/null +++ b/nix/static.nix @@ -0,0 +1,20 @@ +{ + lib, + pkgsCross, + teawie-bot, +}: +let + crossPkgsFor = with pkgsCross; { + x86_64 = musl64.pkgsStatic; + aarch64 = aarch64-multiplatform.pkgsStatic; + }; +in +{ arch }: +let + crossPkgs = crossPkgsFor.${arch}; +in +(crossPkgs.callPackage ./derivation.nix { optimizeSize = true; }).overrideAttrs (old: { + passthru = old.passthru or { } // { + inherit crossPkgs; + }; +}) diff --git a/src/api/guzzle.rs b/src/api/guzzle.rs deleted file mode 100644 index 9ad6ad6..0000000 --- a/src/api/guzzle.rs +++ /dev/null @@ -1,20 +0,0 @@ -use eyre::Result; -use log::debug; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -struct RandomTeawieResponse { - url: String, -} - -// TODO: read this from an env var -const GUZZLE: &str = "https://api.getchoo.com"; -const RANDOM_TEAWIE: &str = "/random_teawie"; - -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?; - - Ok(json.url) -} diff --git a/src/api/mod.rs b/src/api/mod.rs deleted file mode 100644 index dac9209..0000000 --- a/src/api/mod.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::sync::OnceLock; - -use eyre::Result; -use reqwest::Client; -use serde::de::DeserializeOwned; - -pub mod guzzle; -pub mod shiggy; - -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()) -} - -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?; - - Ok(json) -} diff --git a/src/api/shiggy.rs b/src/api/shiggy.rs deleted file mode 100644 index d6a6238..0000000 --- a/src/api/shiggy.rs +++ /dev/null @@ -1,20 +0,0 @@ -use eyre::Result; -use log::debug; -use serde::Deserialize; - -const SHIGGY: &str = "https://safebooru.donmai.us"; -const RANDOM_SHIGGY: &str = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; - -#[derive(Deserialize)] -struct SafebooruResponse { - file_url: String, -} - -#[allow(clippy::module_name_repetitions)] -pub async fn random_shiggy() -> Result<String> { - let url = format!("{SHIGGY}{RANDOM_SHIGGY}"); - debug!("Making request to {url}"); - - let resp: SafebooruResponse = super::get_json(&url).await?; - Ok(resp.file_url) -} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 0000000..65c221b --- /dev/null +++ b/src/client.rs @@ -0,0 +1,99 @@ +use crate::{commands, events, http, storage::Storage}; + +use std::{sync::Arc, time::Duration}; + +use eyre::{bail, Context as _, Result}; +use log::{info, trace, warn}; +use poise::{ + serenity_prelude::{self as serenity}, + EditTracker, Framework, FrameworkOptions, PrefixFrameworkOptions, +}; + +pub type Error = eyre::Report; +pub type Context<'a> = poise::Context<'a, Data, Error>; + +#[derive(Clone, Debug, Default)] +pub struct Data { + pub http_client: http::Client, + pub storage: Option<Storage>, +} + +async fn setup(ctx: &serenity::Context) -> Result<Data> { + let storage = Storage::from_env().ok(); + + if let Some(storage) = storage.as_ref() { + if !storage.clone().is_connected() { + bail!("You specified a storage backend but there's no connection! Is it running?"); + } + trace!("Storage backend connected!"); + + poise::builtins::register_globally(ctx, &commands::global()).await?; + info!("Registered global commands!"); + + // 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?; + + info!("Registered guild commands to {}", guild); + } + } else { + warn!("No storage backend was specified. Features requiring storage cannot be used"); + warn!("Registering optional commands globally since there's no storage backend"); + poise::builtins::register_globally(ctx, &commands::all()).await?; + } + + let http_client = <http::Client as http::Ext>::default(); + let data = Data { + http_client, + storage, + }; + + Ok(data) +} + +pub 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!"); +} + +pub async fn get() -> Result<serenity::Client> { + 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: commands::all(), + on_error: |error| Box::pin(events::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(events::handle(ctx, event, data)), + + prefix_options: PrefixFrameworkOptions { + prefix: Some("!".into()), + edit_tracker: Some(Arc::new(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))) + .build(); + + let client = serenity::ClientBuilder::new(token, intents) + .framework(framework) + .await?; + + Ok(client) +} diff --git a/src/commands/general/ask.rs b/src/commands/general/ask.rs index c715e3a..1300e97 100644 --- a/src/commands/general/ask.rs +++ b/src/commands/general/ask.rs @@ -1,6 +1,6 @@ -use crate::{consts, utils, Context, Error}; +use crate::{client::Context, consts, utils}; -use eyre::Context as _; +use eyre::{Context as _, Result}; /// Ask teawie a question! #[poise::command(prefix_command, slash_command)] @@ -10,7 +10,7 @@ pub async fn ask( #[rename = "question"] #[description = "The question you want to ask teawie"] _question: String, -) -> Result<(), Error> { +) -> Result<()> { 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 54ee0dc..28fdf0d 100644 --- a/src/commands/general/bing.rs +++ b/src/commands/general/bing.rs @@ -1,8 +1,10 @@ -use crate::{Context, Error}; +use crate::client::Context; + +use eyre::Result; /// Make sure the wie is alive #[poise::command(prefix_command)] -pub async fn bing(ctx: Context<'_>) -> Result<(), Error> { +pub async fn bing(ctx: Context<'_>) -> Result<()> { ctx.say("bong!").await?; Ok(()) } diff --git a/src/commands/general/config.rs b/src/commands/general/config.rs index 456e791..6adb78b 100644 --- a/src/commands/general/config.rs +++ b/src/commands/general/config.rs @@ -1,5 +1,5 @@ +use crate::client::Context; use crate::storage::settings::{Properties, Settings}; -use crate::{Context, Error}; use std::str::FromStr; @@ -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(_: Context<'_>) -> Result<(), Error> { +pub async fn config(_: Context<'_>) -> Result<()> { Ok(()) } @@ -72,7 +72,7 @@ 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<(), Error> { +) -> Result<()> { if let Some(storage) = &ctx.data().storage { let gid = ctx.guild_id().unwrap_or_default(); let mut settings = storage.get_guild_settings(&gid).await?; @@ -149,7 +149,7 @@ pub async fn set( pub async fn get( ctx: Context<'_>, #[description = "The setting you want to get"] setting: Properties, -) -> Result<(), Error> { +) -> Result<()> { let gid = &ctx .guild_id() .ok_or_eyre("Failed to get GuildId from context!")?; diff --git a/src/commands/general/convert.rs b/src/commands/general/convert.rs index 4d38eb2..b5e7018 100644 --- a/src/commands/general/convert.rs +++ b/src/commands/general/convert.rs @@ -1,4 +1,4 @@ -use crate::{Context, Error}; +use crate::client::Context; 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(_: Context<'_>) -> Result<(), Error> { +pub async fn convert(_: Context<'_>) -> Result<()> { Ok(()) } @@ -18,7 +18,7 @@ pub async fn convert(_: Context<'_>) -> Result<(), Error> { pub async fn to_celsius( ctx: Context<'_>, #[description = "What teawie will convert"] degrees_fahrenheit: f32, -) -> Result<(), Error> { +) -> Result<()> { 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<(), Error> { +) -> Result<()> { 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<(), Error> { +) -> Result<()> { 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<(), Error> { +) -> Result<()> { let resp: String; if let Ok(decoded) = bottom::decode_string(&message.clone()) { diff --git a/src/commands/general/emoji.rs b/src/commands/general/emoji.rs index 81cd9a3..bbae0b5 100644 --- a/src/commands/general/emoji.rs +++ b/src/commands/general/emoji.rs @@ -1,5 +1,6 @@ -use crate::{consts::Colors, Context, Error}; +use crate::{client::Context, consts::Colors}; +use eyre::Result; use poise::{ serenity_prelude::{CreateEmbed, Emoji}, CreateReply, @@ -7,7 +8,7 @@ use poise::{ /// Get the URL for an emoji #[poise::command(slash_command)] -pub async fn emoji(ctx: Context<'_>, emoji: Emoji) -> Result<(), Error> { +pub async fn emoji(ctx: Context<'_>, emoji: Emoji) -> Result<()> { let url = emoji.url(); let embed = CreateEmbed::new() .title(emoji.name) diff --git a/src/commands/general/pfp.rs b/src/commands/general/pfp.rs index 2ad062b..34ae795 100644 --- a/src/commands/general/pfp.rs +++ b/src/commands/general/pfp.rs @@ -1,13 +1,14 @@ +use crate::{client::Context, consts::Colors}; + +use eyre::Result; use poise::{ serenity_prelude::{CreateEmbed, User}, CreateReply, }; -use crate::{consts::Colors, Context, Error}; - /// Get someone's profile pic #[poise::command(context_menu_command = "Get profile picture", slash_command)] -pub async fn pfp(ctx: Context<'_>, user: User) -> Result<(), Error> { +pub async fn pfp(ctx: Context<'_>, user: User) -> Result<()> { let url = user .avatar_url() .unwrap_or_else(|| user.default_avatar_url()); diff --git a/src/commands/general/random.rs b/src/commands/general/random.rs index 92e9188..094123b 100644 --- a/src/commands/general/random.rs +++ b/src/commands/general/random.rs @@ -1,14 +1,16 @@ -use crate::{api, consts, utils, Context, Error}; +use crate::{client::Context, consts, http, utils}; + +use eyre::Result; #[poise::command(slash_command, subcommands("lore", "teawie", "shiggy"))] #[allow(clippy::unused_async)] -pub async fn random(_: Context<'_>) -> Result<(), Error> { +pub async fn random(_: Context<'_>) -> Result<()> { Ok(()) } /// Get a random piece of teawie lore! #[poise::command(prefix_command, slash_command)] -pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { +pub async fn lore(ctx: Context<'_>) -> Result<()> { let resp = utils::random_choice(consts::LORE)?; ctx.say(resp).await?; @@ -17,8 +19,8 @@ pub async fn lore(ctx: Context<'_>) -> Result<(), Error> { /// Get a random teawie #[poise::command(prefix_command, slash_command)] -pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { - let url = api::guzzle::random_teawie().await?; +pub async fn teawie(ctx: Context<'_>) -> Result<()> { + let url = http::teawie::random(&ctx.data().http_client).await?; utils::send_url_as_embed(ctx, url).await?; Ok(()) @@ -26,8 +28,8 @@ pub async fn teawie(ctx: Context<'_>) -> Result<(), Error> { /// Get a random shiggy #[poise::command(prefix_command, slash_command)] -pub async fn shiggy(ctx: Context<'_>) -> Result<(), Error> { - let url = api::shiggy::random_shiggy().await?; +pub async fn shiggy(ctx: Context<'_>) -> Result<()> { + let url = http::shiggy::random(&ctx.data().http_client).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 5f8eac9..bdf6805 100644 --- a/src/commands/general/version.rs +++ b/src/commands/general/version.rs @@ -1,12 +1,13 @@ -use crate::{consts::Colors, Context, Error}; +use crate::{client::Context, consts::Colors}; use std::env::consts::{ARCH, OS}; +use eyre::Result; use poise::{serenity_prelude::CreateEmbed, CreateReply}; /// Get version info #[poise::command(slash_command)] -pub async fn version(ctx: Context<'_>) -> Result<(), Error> { +pub async fn version(ctx: Context<'_>) -> Result<()> { let sha = option_env!("GIT_SHA").unwrap_or("main"); let revision_url = format!( "[{}]({}/tree/{})", diff --git a/src/commands/mod.rs b/src/commands/mod.rs index e8cac33..b8d0381 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,4 +1,4 @@ -use crate::{Data, Error}; +use crate::client::{Data, Error}; mod general; mod moderation; @@ -17,24 +17,13 @@ macro_rules! cmd { }; } -pub fn to_vec() -> Vec<Command> { - vec![ - cmd!(general, ask), - cmd!(general, bing), - cmd!(general, config), - cmd!(general, convert), - cmd!(general, emoji), - cmd!(general, pfp), - cmd!(general, random), - cmd!(general, version), - cmd!(moderation, clear_messages), - cmd!(optional, copypasta), - cmd!(optional, teawiespam), - cmd!(optional, uwurandom), - ] +pub fn all() -> Vec<Command> { + let mut all_commands = global(); + all_commands.append(&mut optional()); + all_commands } -pub fn to_vec_global() -> Vec<Command> { +pub fn global() -> Vec<Command> { vec![ cmd!(general, ask), cmd!(general, bing), @@ -48,10 +37,6 @@ pub fn to_vec_global() -> Vec<Command> { ] } -pub fn to_vec_optional() -> Vec<Command> { - vec![ - cmd!(optional, copypasta), - cmd!(optional, teawiespam), - cmd!(optional, uwurandom), - ] +pub fn optional() -> Vec<Command> { + vec![cmd!(optional, teawiespam), cmd!(optional, uwurandom)] } diff --git a/src/commands/moderation/clear_messages.rs b/src/commands/moderation/clear_messages.rs index 8761bcb..65a30be 100644 --- a/src/commands/moderation/clear_messages.rs +++ b/src/commands/moderation/clear_messages.rs @@ -1,5 +1,6 @@ -use crate::{Context, Error}; +use crate::client::Context; +use eyre::Result; use log::debug; use poise::serenity_prelude::GetMessages; @@ -13,7 +14,7 @@ use poise::serenity_prelude::GetMessages; pub async fn clear_messages( ctx: Context<'_>, #[description = "How many messages to delete"] num_messages: u8, -) -> Result<(), Error> { +) -> Result<()> { ctx.defer_ephemeral().await?; let channel = ctx.channel_id(); diff --git a/src/commands/optional/copypasta.rs b/src/commands/optional/copypasta.rs deleted file mode 100644 index 06440b1..0000000 --- a/src/commands/optional/copypasta.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{Context, Error}; - -use include_dir::{include_dir, Dir}; -use log::debug; - -const COPYPASTAS: Dir = include_dir!("src/copypastas"); - -#[derive(Debug, poise::ChoiceParameter)] -pub enum Copypasta { - Astral, - Dvd, - Egrill, - HappyMeal, - Sus, - TickTock, - 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() - } -} - -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()) - } -} - -/// ask teawie to send funni copypasta -#[poise::command(slash_command)] -pub async fn copypasta( - ctx: Context<'_>, - #[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?; - - 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"); - } - - 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 95c39bd..a3d1bd2 100644 --- a/src/commands/optional/mod.rs +++ b/src/commands/optional/mod.rs @@ -1,3 +1,2 @@ -pub mod copypasta; pub mod teawiespam; pub mod uwurandom; diff --git a/src/commands/optional/teawiespam.rs b/src/commands/optional/teawiespam.rs index 3a9a387..bfac852 100644 --- a/src/commands/optional/teawiespam.rs +++ b/src/commands/optional/teawiespam.rs @@ -1,10 +1,11 @@ -use crate::{Context, Error}; +use crate::client::Context; +use eyre::Result; use log::debug; /// teawie will spam you. #[poise::command(slash_command)] -pub async fn teawiespam(ctx: Context<'_>) -> Result<(), Error> { +pub async fn teawiespam(ctx: Context<'_>) -> Result<()> { if let Some(guild_id) = ctx.guild_id() { if let Some(storage) = &ctx.data().storage { let settings = storage.get_guild_settings(&guild_id).await?; diff --git a/src/commands/optional/uwurandom.rs b/src/commands/optional/uwurandom.rs index e717d5e..c952dee 100644 --- a/src/commands/optional/uwurandom.rs +++ b/src/commands/optional/uwurandom.rs @@ -1,4 +1,4 @@ -use crate::{Context, Error}; +use crate::client::Context; use eyre::Result; use log::debug; @@ -12,7 +12,7 @@ pub async fn uwurandom( #[min = 1] #[max = 2000] length: Option<u16>, -) -> Result<(), Error> { +) -> Result<()> { if let Some(guild_id) = ctx.guild_id() { if let Some(storage) = &ctx.data().storage { let settings = storage.get_guild_settings(&guild_id).await?; diff --git a/src/copypastas/astral.txt b/src/copypastas/astral.txt deleted file mode 100644 index 9984f31..0000000 --- a/src/copypastas/astral.txt +++ /dev/null @@ -1,7 +0,0 @@ -Today while astral projecting I summoned allah to try and weaken him so our hexing spells would work better. - -He is so fucking powerful. Iโm not at a power level to do this alone. I barely escaped with my life and Iโm spiritually injured to a great amount, but I think Iโll make it. - -I canโt imagine what he would do to a new, unsuspecting witch. Iโm scared that I will have to face him again soon if I ever want to continue astral projecting. Iโm currently burning healing incense and drawing spiritual energy from my crystals to try and heal as quickly as possible. - -Please be safe everyone. Allah is much stronger than I first imagined and we will have to do this together if we want to slay a god. diff --git a/src/copypastas/dvd.txt b/src/copypastas/dvd.txt deleted file mode 100644 index 16d396b..0000000 --- a/src/copypastas/dvd.txt +++ /dev/null @@ -1 +0,0 @@ -๐๐๐๐จ ๐ฟ๐๐จ๐ฃ๐๐ฎ ๐ฟ๐๐ฟ ๐๐จ ๐๐ฃ๐๐๐ฃ๐๐๐ ๐ฌ๐๐ฉ๐ ๐ฟ๐๐จ๐ฃ๐๐ฎโ๐จ ๐๐๐จ๐ฉ๐๐ก๐๐ฎ. ๐๐ค๐ช๐ง ๐ข๐ค๐ซ๐๐ ๐๐ฃ๐ ๐ ๐จ๐๐ก๐๐๐ฉ๐๐ค๐ฃ ๐ค๐ ๐๐ค๐ฃ๐ช๐จ ๐๐๐๐ฉ๐ช๐ง๐๐จ ๐ฌ๐๐ก๐ก ๐๐๐๐๐ฃ ๐๐ช๐ฉ๐ค๐ข๐๐ฉ๐๐๐๐ก๐ก๐ฎ. ๐๐ค ๐๐ฎ๐ฅ๐๐จ๐จ ๐๐๐จ๐ฉ ๐๐ก๐๐ฎ, ๐จ๐๐ก๐๐๐ฉ ๐ฉ๐๐ ๐๐๐๐ฃ ๐๐๐ฃ๐ช ๐๐ช๐ฉ๐ฉ๐ค๐ฃ ๐๐ฉ ๐๐ฃ๐ฎ ๐ฉ๐๐ข๐. ๐๐๐จ๐ฉ ๐๐ก๐๐ฎ ๐ฌ๐๐ก๐ก ๐๐๐๐๐ฃ ๐๐ฃ ๐ ๐ข๐ค๐ข๐๐ฃ๐ฉโฆ diff --git a/src/copypastas/egrill.txt b/src/copypastas/egrill.txt deleted file mode 100644 index 02919b8..0000000 --- a/src/copypastas/egrill.txt +++ /dev/null @@ -1 +0,0 @@ -Everyone jokes about grilling, but they all forget about people like me. I have existed in a constant state of grilling since a BBQ in 1997. I have been attending this charcoal grill for twenty-two years. Do you genuinely think I can make time to argue about politics? Don't make me laugh. I have been flipping this exact burger for a third of my life. It's been decades why is it still raw. It's literally not cooking and I'm not allowed to leave. It's been over a fucking a flame for 22 years and it's still fucking frozen. What fucking vengeful god cursed me with this. help diff --git a/src/copypastas/happymeal.txt b/src/copypastas/happymeal.txt deleted file mode 100644 index bf52100..0000000 --- a/src/copypastas/happymeal.txt +++ /dev/null @@ -1 +0,0 @@ -OH MY GOD ITS 3 IN THE MORNING AND IM IN MCDONALDS AND WE JUST FOUND OUT THAT WHEN U PULL UP IN MCDONALDS AT 3 AM YOU CAN BUY THE AMONG US HAPPY MEAL WITH A TOY IN IT WHICH IS EITHER THE IMPOSTOR OR THE CREWMATE AND IF YOU DONT KNOW WHAT AMONG US IS YOU MUST BE MUST REALLY BE LIVING UNDER A ROCK ITS AN AWESOME GAME WITH IMPOSTORS AND CREWMATES AND BASICALLY THE IMPOSTOR TRIES TO SABOTAGE THE WHOLE GAME AND THE CREWMATES NEED TO STOP HIM BUT APPARENTLY WHEN YOU PURCHASE THE AMONG US HAPPY MEAL SOMETHING SCARY HAPPENS diff --git a/src/copypastas/sus.txt b/src/copypastas/sus.txt deleted file mode 100644 index 83deb72..0000000 --- a/src/copypastas/sus.txt +++ /dev/null @@ -1 +0,0 @@ -HOLY SHIT DID YOU JUST SAY THE WORD SUS???๐ณ1?/1๐ฑ//1๐ณ/1111!!!! Wait, you don't know what it is from?๐ณ๐ณ๐ณLet ๐give you a brief r/history. ๐๐๐๐จโ๐If you didn't r/knowyourshit, the r/term sus(suspicious) is a saying from the r/popular r/game r/AmongUs. Among us is so fun๐ ๐๐, don't insult it, every youtuber and streamer says so!!!!!!!11 Corpses voice is so deep am i right or am i right๐ณ๐ณ????? I mean Mr beast and Dream play and pull big ๐ง 1000000000000 iq moves in their videos..... YOU WERE THE IMPOSTER.... เถ เถ เถ Get it because you don't know what sus means? r/stupidquestions r/youranidot r/stupidcuck. I CAnT BELEeVE YOUU dont KNoW WHT SUS MeaNS?/??!??!?!!๐๐๐๐๐ Man why do i have to r/explain this to a r/idiot๐คช๐คช๐คช๐๐๐... Sus is a GREAT WORD from a GREAT VIDEO GAME. in class, YOU CAN PLAY IT ON YOUR PHONE๐๐๐๐๐๐??!?!? such a masterpiece... FOR THE GREAT PRICE OF FREE!!!11!๐ฐ๐ฐ๐ค๐ค๐ค๐ค๐๐๐๐ฐ๐ฐ It can also mean gay ๐ณ๐ณ๐ณ๐ณ diff --git a/src/copypastas/ticktock.txt b/src/copypastas/ticktock.txt deleted file mode 100644 index bd4f36e..0000000 --- a/src/copypastas/ticktock.txt +++ /dev/null @@ -1,8 +0,0 @@ -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/copypastas/twitter.txt b/src/copypastas/twitter.txt deleted file mode 100644 index 883cd1b..0000000 --- a/src/copypastas/twitter.txt +++ /dev/null @@ -1,35 +0,0 @@ -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm -Twitter's Recommendation Algorithm - diff --git a/src/handlers/error.rs b/src/events/error.rs index e706fec..d98bb5c 100644 --- a/src/handlers/error.rs +++ b/src/events/error.rs @@ -1,4 +1,7 @@ -use crate::{consts::Colors, Data, Error}; +use crate::{ + client::{Data, Error}, + consts::Colors, +}; use log::error; use poise::serenity_prelude::{CreateEmbed, Timestamp}; diff --git a/src/handlers/event/guild.rs b/src/events/guild.rs index 774179c..06af978 100644 --- a/src/handlers/event/guild.rs +++ b/src/events/guild.rs @@ -1,10 +1,10 @@ +use crate::{client::Data, storage}; +use storage::settings::Settings; + use eyre::Result; use log::{debug, warn}; use poise::serenity_prelude::{Guild, UnavailableGuild}; -use crate::{storage, Data}; -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? { diff --git a/src/handlers/event/message.rs b/src/events/message.rs index 67dbb21..e115eb9 100644 --- a/src/handlers/event/message.rs +++ b/src/events/message.rs @@ -1,4 +1,4 @@ -use crate::{consts, Data}; +use crate::{client::Data, consts}; use eyre::{eyre, Result}; use log::{debug, warn}; diff --git a/src/handlers/event/mod.rs b/src/events/mod.rs index cc7d727..390c3a8 100644 --- a/src/handlers/event/mod.rs +++ b/src/events/mod.rs @@ -1,16 +1,17 @@ -use crate::{consts, Data, Error}; +use crate::{client::Data, consts}; use eyre::Result; use log::{debug, info}; use poise::serenity_prelude::{self as serenity, CreateBotAuthParameters}; use serenity::FullEvent; +pub mod error; mod guild; mod message; mod pinboard; mod reactboard; -pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<(), Error> { +pub async fn handle(ctx: &serenity::Context, event: &FullEvent, data: &Data) -> Result<()> { match event { FullEvent::Ready { data_about_bot } => { info!("Logged in as {}!", data_about_bot.user.name); diff --git a/src/handlers/event/pinboard.rs b/src/events/pinboard.rs index 5b7d454..bb0dfe0 100644 --- a/src/handlers/event/pinboard.rs +++ b/src/events/pinboard.rs @@ -1,4 +1,4 @@ -use crate::{utils, Data}; +use crate::{client::Data, utils}; use eyre::{eyre, Context as _, OptionExt as _, Result}; use log::{debug, warn}; diff --git a/src/handlers/event/reactboard.rs b/src/events/reactboard.rs index 75fc858..c27bd80 100644 --- a/src/handlers/event/reactboard.rs +++ b/src/events/reactboard.rs @@ -1,4 +1,4 @@ -use crate::{storage, utils, Data}; +use crate::{client::Data, storage, utils}; use storage::reactboard::ReactBoardEntry; use eyre::{eyre, Context as _, Result}; diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs deleted file mode 100644 index 1610d23..0000000 --- a/src/handlers/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod error; -pub mod event; diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 0000000..0f16852 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,43 @@ +use eyre::Result; +use log::trace; +use serde::de::DeserializeOwned; + +pub mod shiggy; +pub mod teawie; + +pub type Client = reqwest::Client; +pub type Response = reqwest::Response; + +/// Primary extensions for HTTP Client +pub trait Ext { + async fn get_request(&self, url: &str) -> Result<Response>; + async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T>; + fn default() -> Self; +} + +impl Ext for Client { + fn default() -> Self { + reqwest::ClientBuilder::new() + .user_agent(format!( + "teawie-bot/{}", + option_env!("CARGO_PKG_VERSION").unwrap_or("development") + )) + .build() + .unwrap() + } + + async fn get_request(&self, url: &str) -> Result<Response> { + trace!("Making request to {url}"); + let resp = self.get(url).send().await?; + resp.error_for_status_ref()?; + + Ok(resp) + } + + async fn get_json<T: DeserializeOwned>(&self, url: &str) -> Result<T> { + let resp = self.get_request(url).await?; + let json = resp.json().await?; + + Ok(json) + } +} diff --git a/src/http/shiggy.rs b/src/http/shiggy.rs new file mode 100644 index 0000000..397d397 --- /dev/null +++ b/src/http/shiggy.rs @@ -0,0 +1,20 @@ +use eyre::Result; +use serde::Deserialize; + +const SHIGGY: &str = "https://safebooru.donmai.us"; +const RANDOM: &str = "/posts/random.json?tags=kemomimi-chan_(naga_u)+naga_u&only=file_url"; + +#[derive(Deserialize)] +struct SafebooruResponse { + file_url: String, +} + +pub async fn random<T>(http: &T) -> Result<String> +where + T: super::Ext, +{ + let url = format!("{SHIGGY}{RANDOM}"); + let resp: SafebooruResponse = http.get_json(&url).await?; + + Ok(resp.file_url) +} diff --git a/src/http/teawie.rs b/src/http/teawie.rs new file mode 100644 index 0000000..368fad5 --- /dev/null +++ b/src/http/teawie.rs @@ -0,0 +1,28 @@ +use eyre::{bail, OptionExt, Result}; +use serde::{Deserialize, Serialize}; + +// https://github.com/getchoo/teawieAPI +#[derive(Deserialize, Serialize)] +struct RandomTeawieResponse { + url: Option<String>, + error: Option<String>, +} + +// TODO: read this from an env var +const TEAWIE: &str = "https://api.getchoo.com"; +const RANDOM: &str = "/random_teawie"; + +pub async fn random<T>(http: &T) -> Result<String> +where + T: super::Ext, +{ + let url = format!("{TEAWIE}{RANDOM}"); + let json: RandomTeawieResponse = http.get_json(&url).await?; + + if let Some(error) = json.error { + bail!("TeawieAPI reported error: {error}"); + }; + + json.url + .ok_or_eyre("TeawieAPI didn't return an error or URL???") +} diff --git a/src/main.rs b/src/main.rs index 7f19b9e..e91c8e6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,72 +1,18 @@ -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 client; mod commands; mod consts; -mod handlers; +mod events; +mod http; mod storage; mod utils; -use storage::Storage; - -type Error = Box<dyn std::error::Error + Send + Sync>; -type Context<'a> = poise::Context<'a, Data, Error>; - -#[derive(Clone, Debug, Default)] -pub struct Data { - storage: Option<Storage>, -} - -async fn setup(ctx: &serenity::Context) -> Result<Data, Error> { - let storage = Storage::from_env().ok(); - - 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, &commands::to_vec_global()).await?; - info!("Registered global commands!"); - - // 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::to_vec_optional(), guild).await?; - - 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) -} +use eyre::{Report, Result}; -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!"); -} +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; #[tokio::main] async fn main() -> Result<()> { @@ -74,44 +20,9 @@ async fn main() -> Result<()> { color_eyre::install()?; env_logger::init(); - 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: 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::event::handle(ctx, event, data)) - }, - - prefix_options: PrefixFrameworkOptions { - prefix: Some("!".into()), - edit_tracker: Some(Arc::new(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))) - .build(); - - let mut client = serenity::ClientBuilder::new(token, intents) - .framework(framework) - .await?; + let mut client = client::get().await?; - let shard_manager = client.shard_manager.clone(); + let shard_manager = client.shard_manager.clone(); // We need this to shut down the bot #[cfg(target_family = "unix")] let mut sigterm = signal(SignalKind::terminate())?; #[cfg(target_family = "windows")] @@ -120,11 +31,13 @@ async fn main() -> Result<()> { tokio::select! { result = client.start() => result.map_err(Report::from), _ = sigterm.recv() => { - handle_shutdown(shard_manager, "Received SIGTERM").await; + client::handle_shutdown(shard_manager, "Received SIGTERM").await; + println!("Everything is shutdown. Goodbye!"); std::process::exit(0); }, _ = ctrl_c() => { - handle_shutdown(shard_manager, "Interrupted").await; + client::handle_shutdown(shard_manager, "Interrupted").await; + println!("Everything is shutdown. Goodbye!"); std::process::exit(130); } } diff --git a/src/utils.rs b/src/utils.rs index 9b642a7..3cab8c3 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use crate::{consts::Colors, Context}; +use crate::{client::Context, consts::Colors}; use color_eyre::eyre::{eyre, Result}; use poise::serenity_prelude::{self as serenity, CreateEmbedAuthor, CreateEmbedFooter}; diff --git a/treefmt.nix b/treefmt.nix new file mode 100644 index 0000000..81102bc --- /dev/null +++ b/treefmt.nix @@ -0,0 +1,11 @@ +{ + projectRootFile = ".git/config"; + + programs = { + actionlint.enable = true; + deadnix.enable = true; + nixfmt.enable = true; + rustfmt.enable = true; + statix.enable = true; + }; +} |
