diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index edd3c77df..cb5ce6571 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,7 +8,7 @@ jobs: name: Build strategy: matrix: - rust-version: ["nightly", "stable", "1.75", "1.76"] + rust-version: ["nightly", "stable"] cache-key: [""] runs-on: ubuntu-latest # redis: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 1ee2a6f5e..b2799ea58 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -19,11 +19,11 @@ jobs: - name: "Install Rust" run: | rustup toolchain install ${{ matrix.rust-version }} --profile minimal --no-self-update - rustup toolchain install nightly --profile minimal --no-self-update + # rustup toolchain install nightly --profile minimal --no-self-update rustup default ${{ matrix.rust-version }} rustup update rustup component add rustfmt clippy --toolchain ${{ matrix.rust-version }} - rustup component add rustfmt clippy --toolchain nightly + # rustup component add rustfmt clippy --toolchain nightly shell: bash - uses: ikalnytskyi/action-setup-postgres@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..29f888030 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,286 @@ +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with cargo-dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release + +permissions: + contents: write + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't cargo-dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (cargo-dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'cargo dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cargo-dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.17.0/cargo-dist-installer.sh | sh" + - name: Cache cargo-dist + uses: actions/upload-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/cargo-dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "cargo dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by cargo-dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to cargo dist + # - install-dist: expression to run to install cargo-dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: swatinem/rust-cache@v2 + with: + key: ${{ join(matrix.targets, '-') }} + cache-provider: ${{ matrix.cache_provider }} + - name: Install cargo-dist + run: ${{ matrix.install_dist }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "cargo dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached cargo-dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/cargo-dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "cargo dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v4 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-20.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install cached cargo-dist + uses: actions/download-artifact@v4 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/cargo-dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v4 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v4 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-20.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive diff --git a/.gitignore b/.gitignore index 46ab3432b..fd1e57763 100644 --- a/.gitignore +++ b/.gitignore @@ -19,10 +19,12 @@ Secrets.toml .env.old cracktunes.env cracktunes.toml +dotenv +dist-manifest.json # IDE config #.vscode/ # code coverage cobertura.xml -**/*.profraw \ No newline at end of file +**/*.profraw diff --git a/.sqlx/query-dd00c051149a6cd41760e6d8c4d42cc26d31879966ce0f5e079d225e28fb527c.json b/.sqlx/query-1d955f212eda508d26bab0a218e36a30ffe2db30c7869f5129a05eb1bbd74619.json similarity index 93% rename from .sqlx/query-dd00c051149a6cd41760e6d8c4d42cc26d31879966ce0f5e079d225e28fb527c.json rename to .sqlx/query-1d955f212eda508d26bab0a218e36a30ffe2db30c7869f5129a05eb1bbd74619.json index ea0859c7d..522c33460 100644 --- a/.sqlx/query-dd00c051149a6cd41760e6d8c4d42cc26d31879966ce0f5e079d225e28fb527c.json +++ b/.sqlx/query-1d955f212eda508d26bab0a218e36a30ffe2db30c7869f5129a05eb1bbd74619.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n select metadata.id, title, artist, album, track, date, channels, channel, start_time, duration, sample_rate, source_url, thumbnail\n from play_log \n join metadata on \n play_log.metadata_id = metadata.id \n where guild_id = $1 order by created_at desc limit 5\n ", + "query": "\n select metadata.id, title, artist, album, track, date, channels, channel, start_time, duration, sample_rate, source_url, thumbnail\n from play_log \n join metadata on \n play_log.metadata_id = metadata.id \n where guild_id = $1 order by created_at desc limit $2\n ", "describe": { "columns": [ { @@ -71,6 +71,7 @@ ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -90,5 +91,5 @@ true ] }, - "hash": "dd00c051149a6cd41760e6d8c4d42cc26d31879966ce0f5e079d225e28fb527c" + "hash": "1d955f212eda508d26bab0a218e36a30ffe2db30c7869f5129a05eb1bbd74619" } diff --git a/.sqlx/query-3bd71bd55228723ef5a485a456972f08ea3912c51e59ef7f065cf99dfffa80e8.json b/.sqlx/query-3bd71bd55228723ef5a485a456972f08ea3912c51e59ef7f065cf99dfffa80e8.json new file mode 100644 index 000000000..0d4df7568 --- /dev/null +++ b/.sqlx/query-3bd71bd55228723ef5a485a456972f08ea3912c51e59ef7f065cf99dfffa80e8.json @@ -0,0 +1,82 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT A.command, permission_settings.* FROM\n (SELECT * FROM command_channel WHERE guild_id = $1) as A\n JOIN permission_settings ON A.permission_settings_id = permission_settings.id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "command", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "default_allow_all_commands", + "type_info": "Bool" + }, + { + "ordinal": 3, + "name": "default_allow_all_users", + "type_info": "Bool" + }, + { + "ordinal": 4, + "name": "default_allow_all_roles", + "type_info": "Bool" + }, + { + "ordinal": 5, + "name": "allowed_roles", + "type_info": "Int8Array" + }, + { + "ordinal": 6, + "name": "denied_roles", + "type_info": "Int8Array" + }, + { + "ordinal": 7, + "name": "allowed_users", + "type_info": "Int8Array" + }, + { + "ordinal": 8, + "name": "denied_users", + "type_info": "Int8Array" + }, + { + "ordinal": 9, + "name": "allowed_channels", + "type_info": "Int8Array" + }, + { + "ordinal": 10, + "name": "denied_channels", + "type_info": "Int8Array" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "3bd71bd55228723ef5a485a456972f08ea3912c51e59ef7f065cf99dfffa80e8" +} diff --git a/.sqlx/query-0e96b214b96766fae1ae31235be40fcc90b8054a8b81e189f3bbe5836a623c0e.json b/.sqlx/query-4a8a694dbba3537b1fa3415b86c3653ea8a5e841e6cf5f8cc2574957a0fa6dae.json similarity index 80% rename from .sqlx/query-0e96b214b96766fae1ae31235be40fcc90b8054a8b81e189f3bbe5836a623c0e.json rename to .sqlx/query-4a8a694dbba3537b1fa3415b86c3653ea8a5e841e6cf5f8cc2574957a0fa6dae.json index a02203b97..ae27e7859 100644 --- a/.sqlx/query-0e96b214b96766fae1ae31235be40fcc90b8054a8b81e189f3bbe5836a623c0e.json +++ b/.sqlx/query-4a8a694dbba3537b1fa3415b86c3653ea8a5e841e6cf5f8cc2574957a0fa6dae.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n select title, artist \n from play_log \n join metadata on \n play_log.metadata_id = metadata.id \n where user_id = $1 order by created_at desc limit 5\n ", + "query": "\n select title, artist \n from play_log \n join metadata on \n play_log.metadata_id = metadata.id \n where user_id = $1 order by created_at desc limit $2\n ", "describe": { "columns": [ { @@ -16,6 +16,7 @@ ], "parameters": { "Left": [ + "Int8", "Int8" ] }, @@ -24,5 +25,5 @@ true ] }, - "hash": "0e96b214b96766fae1ae31235be40fcc90b8054a8b81e189f3bbe5836a623c0e" + "hash": "4a8a694dbba3537b1fa3415b86c3653ea8a5e841e6cf5f8cc2574957a0fa6dae" } diff --git a/.sqlx/query-50749618ad1384a3af0aea8ce5ee50cf9b770882546585f48cc9789648417fb0.json b/.sqlx/query-771325738708d03fa8472ff43da7ab8acb962bf64b19f446bec57f47f8368aaf.json similarity index 77% rename from .sqlx/query-50749618ad1384a3af0aea8ce5ee50cf9b770882546585f48cc9789648417fb0.json rename to .sqlx/query-771325738708d03fa8472ff43da7ab8acb962bf64b19f446bec57f47f8368aaf.json index 16e1e2466..c57fb8be2 100644 --- a/.sqlx/query-50749618ad1384a3af0aea8ce5ee50cf9b770882546585f48cc9789648417fb0.json +++ b/.sqlx/query-771325738708d03fa8472ff43da7ab8acb962bf64b19f446bec57f47f8368aaf.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n select title, artist \n from (play_log\n join metadata on \n play_log.metadata_id = metadata.id)\n left join track_reaction on play_log.id = track_reaction.play_log_id\n where guild_id = $1 and (track_reaction is null or track_reaction.dislikes >= $2)\n order by play_log.created_at desc limit 5\n ", + "query": "\n select title, artist \n from (play_log\n join metadata on \n play_log.metadata_id = metadata.id)\n left join track_reaction on play_log.id = track_reaction.play_log_id\n where guild_id = $1 and (track_reaction is null or track_reaction.dislikes <= $2)\n order by play_log.created_at desc limit $3\n ", "describe": { "columns": [ { @@ -17,7 +17,8 @@ "parameters": { "Left": [ "Int8", - "Int4" + "Int4", + "Int8" ] }, "nullable": [ @@ -25,5 +26,5 @@ true ] }, - "hash": "50749618ad1384a3af0aea8ce5ee50cf9b770882546585f48cc9789648417fb0" + "hash": "771325738708d03fa8472ff43da7ab8acb962bf64b19f446bec57f47f8368aaf" } diff --git a/.sqlx/query-9cdc04e55dd90f1b3d394b9f65363b21a0873144d27c51e9541a44d9bfcc789c.json b/.sqlx/query-9cdc04e55dd90f1b3d394b9f65363b21a0873144d27c51e9541a44d9bfcc789c.json index d8a3b879d..387987465 100644 --- a/.sqlx/query-9cdc04e55dd90f1b3d394b9f65363b21a0873144d27c51e9541a44d9bfcc789c.json +++ b/.sqlx/query-9cdc04e55dd90f1b3d394b9f65363b21a0873144d27c51e9541a44d9bfcc789c.json @@ -42,6 +42,16 @@ "ordinal": 7, "name": "denied_users", "type_info": "Int8Array" + }, + { + "ordinal": 8, + "name": "allowed_channels", + "type_info": "Int8Array" + }, + { + "ordinal": 9, + "name": "denied_channels", + "type_info": "Int8Array" } ], "parameters": { @@ -57,6 +67,8 @@ false, false, false, + false, + false, false ] }, diff --git a/.sqlx/query-ad509ae3e59c771c771a7b5262f9e16e6e69b5f5d548042ef39dcc342ff4975a.json b/.sqlx/query-ad509ae3e59c771c771a7b5262f9e16e6e69b5f5d548042ef39dcc342ff4975a.json new file mode 100644 index 000000000..d49fef2dc --- /dev/null +++ b/.sqlx/query-ad509ae3e59c771c771a7b5262f9e16e6e69b5f5d548042ef39dcc342ff4975a.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO vote_webhook\n (bot_id, user_id, kind, is_weekend, query, created_at)\n VALUES\n ($1, $2, $3, $4, $5, now())\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Int8", + { + "Custom": { + "name": "webhook_kind", + "kind": { + "Enum": [ + "upvote", + "test" + ] + } + } + }, + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "ad509ae3e59c771c771a7b5262f9e16e6e69b5f5d548042ef39dcc342ff4975a" +} diff --git a/.sqlx/query-3e23293d66b2b5dd8173c711c25c7722e51c982ae6d2b25b750b567729d99e08.json b/.sqlx/query-e53e6dc434c93215cd8b56e172a2b128feb89d12e616ed9c78674fcbb7f8cba3.json similarity index 74% rename from .sqlx/query-3e23293d66b2b5dd8173c711c25c7722e51c982ae6d2b25b750b567729d99e08.json rename to .sqlx/query-e53e6dc434c93215cd8b56e172a2b128feb89d12e616ed9c78674fcbb7f8cba3.json index cb24fbc9e..eb12cbc45 100644 --- a/.sqlx/query-3e23293d66b2b5dd8173c711c25c7722e51c982ae6d2b25b750b567729d99e08.json +++ b/.sqlx/query-e53e6dc434c93215cd8b56e172a2b128feb89d12e616ed9c78674fcbb7f8cba3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO permission_settings\n (default_allow_all_commands,\n default_allow_all_users,\n default_allow_all_roles,\n allowed_roles,\n denied_roles,\n allowed_users,\n denied_users)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7)\n RETURNING *", + "query": "INSERT INTO permission_settings\n (default_allow_all_commands,\n default_allow_all_users,\n default_allow_all_roles,\n allowed_roles,\n denied_roles,\n allowed_users,\n denied_users,\n allowed_channels,\n denied_channels)\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9)\n RETURNING *", "describe": { "columns": [ { @@ -42,6 +42,16 @@ "ordinal": 7, "name": "denied_users", "type_info": "Int8Array" + }, + { + "ordinal": 8, + "name": "allowed_channels", + "type_info": "Int8Array" + }, + { + "ordinal": 9, + "name": "denied_channels", + "type_info": "Int8Array" } ], "parameters": { @@ -52,6 +62,8 @@ "Int8Array", "Int8Array", "Int8Array", + "Int8Array", + "Int8Array", "Int8Array" ] }, @@ -63,8 +75,10 @@ false, false, false, + false, + false, false ] }, - "hash": "3e23293d66b2b5dd8173c711c25c7722e51c982ae6d2b25b750b567729d99e08" + "hash": "e53e6dc434c93215cd8b56e172a2b128feb89d12e616ed9c78674fcbb7f8cba3" } diff --git a/.vscode/settings.json b/.vscode/settings.json index f2b80eca4..f82d3ee42 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,25 +12,23 @@ } ], "rust-analyzer.cargo.features": [ - "crack-gpt" + "crack-gpt", + "crack-osint", + "crack-bf", ], - // "crack-osint", - // "crack-core", - // "crack-telemetry", - // "crack-tracing", - // "db", - // "playlist", - // "cache", - // "ignore-presence-log", - // ], - // "rust-analyzer.cargo.features": "all", "rust-analyzer.linkedProjects": [ "./crack-core/Cargo.toml", + "./crack-bf/Cargo.toml", "./crack-gpt/Cargo.toml", - "./crack-osint/Cargo.toml" + "./crack-osint/Cargo.toml", + "./crack-voting/Cargo.toml" ], "sqltools.useNodeRuntime": null, "rust-analyzer.checkOnSave": true, "gitdoc.enabled": false, "editor.fontFamily": "0xProto Nerd Font Mono", + "keyboard.dispatch": "keyCode", + "editor.formatOnSave": true, + //"terminal.integrated.fontFamily": "0xProto Nerd Font Mono" + "terminal.integrated.fontFamily": "monospace" } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 000000000..06aff576f --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,82 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "clippy", + "args": [ + "--all", + "--profile=release", + "--workspace", + "--features", + "crack-bf,crack-osint,crack-gpt", + "--", + "-D", + "clippy::all", + "-D", + "warnings" + ], + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo clippy" + }, + { + "type": "cargo", + "command": "test", + "args": [ + "--package", + "crack-core", + "--lib", + "--features", + "crack-gpt", + "--features", + "crack-osint", + "--features", + "crack-bf", + "--profile=release", + "--", + "--exact", + "--show-output" + ], + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo test crack-core" + }, + { + "type": "cargo", + "command": "check", + "args": [ + "--all", + "--features", + "crack-bf,crack-osint,crack-gpt", + "--profile=release", + "--workspace" + ], + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo check" + }, + { + "type": "cargo", + "command": "build", + "args": [ + "--all", + "--features", + "crack-bf,crack-osint,crack-gpt", + "--profile=release", + "--workspace" + ], + "problemMatcher": [ + "$rustc" + ], + "group": "build", + "label": "rust: cargo build" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index be4798de3..43533978f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c90a406b4495d129f00461241616194cb8a032c8d1c53c657f0961d5f8e0498" +checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" dependencies = [ "flate2", "futures-core", @@ -137,15 +137,16 @@ dependencies = [ [[package]] name = "async-openai" -version = "0.21.0" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "007f03f7e27271451af57ced242d6adfa04204d1275a91ec0952bf441fd8d102" +checksum = "dc0e5ff98f9e7c605df4c88783a0439d1dc667ce86bd79e99d4164f8b0c05ccc" dependencies = [ "async-convert", "backoff", "base64 0.22.1", "bytes", - "derive_builder", + "derive_builder 0.20.0", + "eventsource-stream", "futures", "rand", "reqwest", @@ -179,31 +180,31 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "async-tungstenite" -version = "0.25.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cca750b12e02c389c1694d35c16539f88b8bbaa5945934fdc1b41a776688589" +checksum = "bb786dab48e539c5f17b23bac20d812ac027c01732ed7c7b58850c69a684e46c" dependencies = [ "futures-io", "futures-util", "log", "pin-project-lite", - "tungstenite 0.21.0", + "tungstenite 0.23.0", ] [[package]] @@ -215,6 +216,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "audiopus" version = "0.3.0-rc.0" @@ -257,9 +264,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.72" +version = "0.3.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" dependencies = [ "addr2line", "cc", @@ -302,9 +309,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" dependencies = [ "serde", ] @@ -341,49 +348,44 @@ dependencies = [ [[package]] name = "boa_ast" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6fb81ca0f301f33aff7401e2ffab37dc9e0e4a1cf0ccf6b34f4d9e60aa0682" +checksum = "73498e9b2f0aa7db74977afa4d594657611e90587abf0dd564c0b55b4a130163" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "boa_interner", "boa_macros", - "indexmap 2.2.6", + "indexmap", "num-bigint", "rustc-hash", ] [[package]] name = "boa_engine" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "600e4e4a65b26efcef08a7b1cf2899d3845a32e82e067ee3b75eaf7e413ff31c" +checksum = "16377479d5d6d33896e7acdd1cc698d04a8f72004025bbbddf47558cd29146a6" dependencies = [ - "arrayvec", - "bitflags 2.5.0", + "bitflags 2.6.0", "boa_ast", "boa_gc", + "boa_icu_provider", "boa_interner", "boa_macros", "boa_parser", "boa_profiler", - "bytemuck", - "cfg-if", - "dashmap", + "chrono", + "dashmap 5.5.3", "fast-float", - "hashbrown 0.14.5", "icu_normalizer", - "indexmap 2.2.6", - "intrusive-collections", - "itertools", + "indexmap", + "itertools 0.11.0", "num-bigint", "num-integer", "num-traits", "num_enum", "once_cell", - "paste", "pollster", - "portable-atomic", "rand", "regress", "rustc-hash", @@ -395,31 +397,44 @@ dependencies = [ "tap", "thin-vec", "thiserror", - "time", ] [[package]] name = "boa_gc" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c055ef3cd87ea7db014779195bc90c6adfc35de4902e3b2fe587adecbd384578" +checksum = "c97b44beaef9d4452342d117d94607fdfa8d474280f1ba0fd97853834e3a49b2" dependencies = [ "boa_macros", "boa_profiler", - "hashbrown 0.14.5", "thin-vec", ] +[[package]] +name = "boa_icu_provider" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30e52e34e451dd0bfc2c654a9a43ed34b0073dbd4ae3394b40313edda8627aa" +dependencies = [ + "icu_collections", + "icu_normalizer", + "icu_properties", + "icu_provider", + "icu_provider_adapters", + "icu_provider_blob", + "once_cell", +] + [[package]] name = "boa_interner" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cacc9caf022d92195c827a3e5bf83f96089d4bfaff834b359ac7b6be46e9187" +checksum = "f3e5afa991908cfbe79bd3109b824e473a1dc5f74f31fab91bb44c9e245daa77" dependencies = [ "boa_gc", "boa_macros", "hashbrown 0.14.5", - "indexmap 2.2.6", + "indexmap", "once_cell", "phf 0.11.2", "rustc-hash", @@ -428,46 +443,52 @@ dependencies = [ [[package]] name = "boa_macros" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6be9c93793b60dac381af475b98634d4b451e28336e72218cad9a20176218dbc" +checksum = "005fa0c5bd20805466dda55eb34cd709bb31a2592bb26927b47714eeed6914d8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "synstructure", ] [[package]] name = "boa_parser" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8592556849f0619ed142ce2b3a19086769314a8d657f93a5765d06dbce4818" +checksum = "9e09afb035377a9044443b598187a7d34cd13164617182a4d7c348522ee3f052" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "boa_ast", + "boa_icu_provider", "boa_interner", "boa_macros", "boa_profiler", "fast-float", + "icu_locid", "icu_properties", + "icu_provider", + "icu_provider_macros", "num-bigint", "num-traits", + "once_cell", "regress", "rustc-hash", + "tinystr", ] [[package]] name = "boa_profiler" -version = "0.18.0" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d8372f2d5cbac600a260de87877141b42da1e18d2c7a08ccb493a49cbd55c0" +checksum = "3190f92dfe48224adc92881c620f08ccf37ff62b91a094bb357fe53bd5e84647" [[package]] name = "borsh" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe5b10e214954177fb1dc9fbd20a1a2608fe99e6c832033bdc7cea287a20d77" +checksum = "a6362ed55def622cddc70a4746a68554d7b687713770de539e59a739b249f8ed" dependencies = [ "borsh-derive", "cfg_aliases", @@ -475,15 +496,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a8646f94ab393e43e8b35a2558b1624bed28b97ee09c5d15456e3c9463f46d" +checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", - "proc-macro-crate", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "syn_derive", ] @@ -534,23 +555,9 @@ checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ee891b04274a59bd38b412188e24b849617b2e45a0fd8d057deb63e7403761b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.66", -] +checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e" [[package]] name = "byteorder" @@ -560,9 +567,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "camino" @@ -595,6 +602,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "cbc" version = "0.1.2" @@ -606,9 +627,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.98" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41c270e7540d725e65ac7f1b212ac8ce349719624d7bcff99f8e2e488e8cf03f" +checksum = "9711f33475c22aab363b05564a17d7b789bf3dfec5ebabb586adee56f0e271b5" [[package]] name = "cfg-if" @@ -618,9 +639,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfg_aliases" -version = "0.1.1" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" @@ -634,7 +655,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -657,6 +678,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "colored" version = "2.1.0" @@ -713,9 +740,9 @@ checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" [[package]] name = "cookie" -version = "0.17.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ "percent-encoding", "time", @@ -724,12 +751,12 @@ dependencies = [ [[package]] name = "cookie_store" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa" dependencies = [ "cookie", - "idna 0.3.0", + "idna 0.5.0", "log", "publicsuffix", "serde", @@ -764,9 +791,16 @@ dependencies = [ "libc", ] +[[package]] +name = "crack-bf" +version = "0.1.0" +dependencies = [ + "tokio", +] + [[package]] name = "crack-core" -version = "0.3.7" +version = "0.3.8" dependencies = [ "anyhow", "async-trait", @@ -775,17 +809,19 @@ dependencies = [ "bytes", "chrono", "colored", + "crack-bf", "crack-gpt", "crack-osint", "ctor", + "dashmap 6.0.1", "either", - "ffprobe", + "indexmap", + "itertools 0.13.0", "lazy_static", "lyric_finder", "mockall", "once_cell", "poise", - "proc-macro2", "prometheus", "rand", "regex", @@ -794,18 +830,19 @@ dependencies = [ "rusty_ytdl", "serde", "serde_json", - "serde_with", "serenity", "serenity-voice-model", "songbird", "sqlx", + "strsim 0.11.1", "symphonia", "sys-info", "tokio", "tracing", - "tungstenite 0.21.0", + "tungstenite 0.23.0", "typemap_rev", "url", + "vergen", ] [[package]] @@ -814,9 +851,7 @@ version = "0.2.0" dependencies = [ "async-openai", "const_format", - "ctor", "tokio", - "tracing", "ttl_cache", ] @@ -834,25 +869,38 @@ dependencies = [ "whois-rust", ] +[[package]] +name = "crack-voting" +version = "0.1.0" +dependencies = [ + "chrono", + "dbl-rs", + "lazy_static", + "serde", + "sqlx", + "tokio", + "tracing", + "warp", +] + [[package]] name = "cracktunes" -version = "0.3.7" +version = "0.3.8" dependencies = [ - "async-trait", - "colored", "config-file", "crack-core", - "crack-gpt", - "crack-osint", "dotenvy", + "opentelemetry", + "opentelemetry_sdk", "poise", "prometheus", - "songbird", "sqlx", "tokio", "tracing", - "tracing-appender", + "tracing-bunyan-formatter", "tracing-subscriber", + "vergen", + "warp", ] [[package]] @@ -879,6 +927,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "crossbeam-channel" version = "0.5.13" @@ -949,7 +1003,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -959,42 +1013,77 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" dependencies = [ "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "darling" -version = "0.20.9" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.10.2", + "darling_macro 0.10.2", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] name = "darling_core" -version = "0.20.9" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.9.3", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", - "strsim", - "syn 2.0.66", + "strsim 0.11.1", + "syn 2.0.71", ] [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core 0.10.2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core", + "darling_core 0.20.10", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1011,12 +1100,37 @@ dependencies = [ "serde", ] +[[package]] +name = "dashmap" +version = "6.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804c8821570c3f8b70230c2ba75ffa5c0f9a4189b9a432b6656c536712acae28" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core 0.9.10", +] + [[package]] name = "data-encoding" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "dbl-rs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ef31459083b8ac95c1f31be8f88d124a0d23aa11de592895600ff4b0da2e90" +dependencies = [ + "reqwest", + "serde", + "url", +] + [[package]] name = "der" version = "0.7.9" @@ -1049,6 +1163,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_builder" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2658621297f2cf68762a6f7dc0bb7e1ff2cfd6583daef8ee0fed6f7ec468ec0" +dependencies = [ + "darling 0.10.2", + "derive_builder_core 0.9.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_builder" version = "0.20.0" @@ -1058,16 +1185,28 @@ dependencies = [ "derive_builder_macro", ] +[[package]] +name = "derive_builder_core" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2791ea3e372c8495c0bc2033991d76b512cd799d07491fbd6890124db9458bef" +dependencies = [ + "darling 0.10.2", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "derive_builder_core" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1076,21 +1215,21 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ - "derive_builder_core", - "syn 2.0.66", + "derive_builder_core 0.20.0", + "syn 2.0.71", ] [[package]] name = "derive_more" -version = "0.99.17" +version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ "convert_case", "proc-macro2", "quote", "rustc_version", - "syn 1.0.109", + "syn 2.0.71", ] [[package]] @@ -1117,13 +1256,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1146,9 +1285,9 @@ checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" [[package]] name = "dtoa-short" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" dependencies = [ "dtoa", ] @@ -1162,7 +1301,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1173,13 +1312,19 @@ checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -1201,10 +1346,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1224,7 +1369,7 @@ checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1236,7 +1381,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1310,17 +1455,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" -[[package]] -name = "ffprobe" -version = "0.4.0" -source = "git+https://github.com/cycle-five/ffprobe-rs#e35281a7086cc25c5cfa8380e6e2a1f650501263" -dependencies = [ - "serde", - "serde_json", - "tokio", - "url", -] - [[package]] name = "flate2" version = "1.0.30" @@ -1364,6 +1498,15 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" +[[package]] +name = "from_map" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99f31122ab0445ff8cee420b805f24e07683073815de1dd276ee7d588d301700" +dependencies = [ + "hashmap_derive", +] + [[package]] name = "funty" version = "2.0.0" @@ -1447,7 +1590,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1505,7 +1648,7 @@ dependencies = [ "libc", "log", "rustversion", - "windows", + "windows 0.48.0", ] [[package]] @@ -1519,6 +1662,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "getopts" version = "0.2.21" @@ -1553,6 +1706,44 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1562,6 +1753,15 @@ dependencies = [ "ahash 0.7.8", ] +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -1581,6 +1781,50 @@ dependencies = [ "hashbrown 0.14.5", ] +[[package]] +name = "hashmap_derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb30bf173e72cc31b5265dac095423ca14e7789ff7c3b0e6096a37a996f12883" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 0.2.12", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http 0.2.12", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "heck" version = "0.4.1" @@ -1590,6 +1834,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1611,6 +1861,21 @@ dependencies = [ "hmac", ] +[[package]] +name = "hls_m3u8" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e2bd2de7f92b5301546ce1ac53a4ae5cf2f6b10751b100ab1efb73c19788fba" +dependencies = [ + "derive_builder 0.9.0", + "derive_more", + "hex", + "shorthand", + "stable-vec", + "strum 0.17.1", + "thiserror", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1654,7 +1919,7 @@ dependencies = [ "markup5ever 0.12.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -1681,9 +1946,20 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -1691,34 +1967,65 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.3.1" +version = "0.14.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" dependencies = [ "bytes", "futures-channel", + "futures-core", "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.5", "http 1.1.0", - "http-body", + "http-body 1.0.1", "httparse", "itoa", "pin-project-lite", @@ -1729,33 +2036,35 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.26.0" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", "http 1.1.0", - "hyper", + "hyper 1.4.1", "hyper-util", - "rustls 0.22.4", + "rustls 0.23.11", + "rustls-native-certs 0.7.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tower-service", + "webpki-roots 0.26.3", ] [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -1789,11 +2098,12 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.4.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "137d96353afc8544d437e8a99eceb10ab291352699573b0de5b08bda38c78c60" +checksum = "ef8302d8dfd6044d3ddb3f807a5ef3d7bbca9a574959c6d6e4dc39aa7012d0d5" dependencies = [ "displaydoc", + "serde", "yoke", "zerofrom", "zerovec", @@ -1801,48 +2111,29 @@ dependencies = [ [[package]] name = "icu_locid" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0aa2536adc14c07e2a521e95512b75ed8ef832f0fdf9299d4a0a45d2be2a9d" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.4.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c17d8f6524fdca4471101dd71f0a132eb6382b5d6d7f2970441cb25f6f435a" +checksum = "3003f85dccfc0e238ff567693248c59153a46f4e6125ba4020b973cef4d1d335" dependencies = [ "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", + "litemap", + "serde", "tinystr", + "writeable", "zerovec", ] -[[package]] -name = "icu_locid_transform_data" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c6c3e8bf9580e2dafee8de6f9ec14826aaf359787789c7724f1f85f47d3dc" - [[package]] name = "icu_normalizer" -version = "1.4.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accb85c5b2e76f8dade22978b3795ae1e550198c6cfc7e915144e17cd6e2ab56" +checksum = "652869735c9fb9f5a64ba180ee16f2c848390469c116deef517ecc53f4343598" dependencies = [ "displaydoc", "icu_collections", - "icu_normalizer_data", "icu_properties", "icu_provider", + "serde", "smallvec", "utf16_iter", "utf8_iter", @@ -1850,59 +2141,75 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_normalizer_data" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3744fecc0df9ce19999cdaf1f9f3a48c253431ce1d67ef499128fe9d0b607ab" - [[package]] name = "icu_properties" -version = "1.4.2" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8173ba888885d250016e957b8ebfd5a65cdb690123d8833a19f6833f9c2b579" +checksum = "ce0e1aa26851f16c9e04412a5911c86b7f8768dac8f8d4c5f1c568a7e5d7a434" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", - "icu_properties_data", "icu_provider", + "serde", "tinystr", "zerovec", ] -[[package]] -name = "icu_properties_data" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70a8b51ee5dd4ff8f20ee9b1dd1bc07afc110886a3747b1fec04cc6e5a15815" - [[package]] name = "icu_provider" -version = "1.4.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba58e782287eb6950247abbf11719f83f5d4e4a5c1f2cd490d30a334bc47c2f4" +checksum = "8dc312a7b6148f7dfe098047ae2494d12d4034f48ade58d4f353000db376e305" dependencies = [ "displaydoc", "icu_locid", "icu_provider_macros", + "postcard", + "serde", "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", "zerovec", ] +[[package]] +name = "icu_provider_adapters" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ae1e2bd0c41728b77e7c46e9afdec5e2127d1eedacc684724667d50c126bd3" +dependencies = [ + "icu_locid", + "icu_provider", + "serde", + "tinystr", + "yoke", + "zerovec", +] + +[[package]] +name = "icu_provider_blob" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd364c9a01f791a4bc04a74cf2a1d01d9f6926a40fd5ae1c28004e1e70d8338b" +dependencies = [ + "icu_provider", + "postcard", + "serde", + "writeable", + "yoke", + "zerovec", +] + [[package]] name = "icu_provider_macros" -version = "1.4.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2abdd3a62551e8337af119c5899e600ca0c88ec8f23a46c60ba216c803dcf1a" +checksum = "dd8b728b9421e93eff1d9f8681101b78fa745e0748c95c655c83f337044a7e10" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 1.0.109", ] [[package]] @@ -1941,17 +2248,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -1960,7 +2256,6 @@ checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.5", - "serde", ] [[package]] @@ -1985,15 +2280,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "intrusive-collections" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b694dc9f70c3bda874626d2aed13b780f137aab435f4e9814121955cf706122e" -dependencies = [ - "memoffset", -] - [[package]] name = "ipinfo" version = "3.0.1" @@ -2025,9 +2311,18 @@ dependencies = [ [[package]] name = "itertools" -version = "0.12.1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" dependencies = [ "either", ] @@ -2049,11 +2344,11 @@ dependencies = [ [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin 0.9.8", ] [[package]] @@ -2109,9 +2404,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loom" @@ -2139,8 +2434,8 @@ dependencies = [ [[package]] name = "lyric_finder" -version = "0.1.6" -source = "git+https://github.com/cycle-five/spotify-player?branch=master#10081b5b80986ab7fe8015b1e3ab2dd62ace8438" +version = "0.1.7" +source = "git+https://github.com/cycle-five/spotify-player?branch=master#283e56595747b59716b18752ad8b195478c86482" dependencies = [ "anyhow", "html5ever 0.27.0", @@ -2223,7 +2518,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2238,18 +2533,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" - -[[package]] -name = "memoffset" -version = "0.9.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2259,9 +2545,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" dependencies = [ "mime", "unicase", @@ -2275,7 +2561,7 @@ checksum = "c325dfab65f261f386debee8b0969da215b3fa0037e74c8a1234db7ba986d803" dependencies = [ "crossbeam-channel", "crossbeam-utils", - "dashmap", + "dashmap 5.5.3", "skeptic", "smallvec", "tagptr", @@ -2290,9 +2576,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" dependencies = [ "adler", ] @@ -2332,7 +2618,25 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 0.2.12", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.8", + "version_check", ] [[package]] @@ -2359,6 +2663,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "no-std-compat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df270209a7f04d62459240d890ecb792714d5db12c92937823574a09930276b4" + [[package]] name = "no-std-net" version = "0.6.0" @@ -2381,6 +2691,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2393,9 +2712,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -2476,23 +2795,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2506,9 +2825,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "081b846d1d56ddfc18fdf1a922e4f6e07a11768ea1b92dec44e42b72712ccfce" dependencies = [ "memchr", ] @@ -2518,6 +2837,10 @@ name = "once_cell" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "opaque-debug" @@ -2531,6 +2854,39 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "opentelemetry" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b69a91d4893e713e06f724597ad630f1fa76057a5e1026c0ca67054a9032a76" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae312d58eaa90a82d2e627fd86e075cf5230b3f11794e2ed74199ebbe572d4fd" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "lazy_static", + "once_cell", + "opentelemetry", + "ordered-float 4.2.1", + "percent-encoding", + "rand", + "thiserror", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -2540,6 +2896,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ff2cf528c6c03d9ed653d6c4ce1dc0582dc4af309790ad92f07c1cd551b0be" +dependencies = [ + "num-traits", +] + [[package]] name = "overload" version = "0.1.1" @@ -2589,9 +2954,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.1", + "redox_syscall 0.5.2", "smallvec", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2600,6 +2965,15 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "patricia_tree" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f2f4539bffe53fc4b4da301df49d114b845b077bd5727b7fe2bd9d8df2ae68" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2684,7 +3058,7 @@ dependencies = [ "phf_shared 0.11.2", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2722,7 +3096,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2782,7 +3156,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2797,11 +3171,12 @@ dependencies = [ [[package]] name = "poise" version = "0.6.1" -source = "git+https://github.com/cycle-five/poise?branch=current#6d555f3c92cf61f75e6830d738da77fb31eaaba7" +source = "git+https://github.com/cycle-five/poise?branch=current#b1c3ec1fd3fc8fcc8d77f3a87bb6b3bd75ab3cd7" dependencies = [ "async-trait", "derivative", "futures-util", + "indexmap", "parking_lot 0.12.3", "poise_macros", "regex", @@ -2814,12 +3189,12 @@ dependencies = [ [[package]] name = "poise_macros" version = "0.6.1" -source = "git+https://github.com/cycle-five/poise?branch=current#6d555f3c92cf61f75e6830d738da77fb31eaaba7" +source = "git+https://github.com/cycle-five/poise?branch=current#b1c3ec1fd3fc8fcc8d77f3a87bb6b3bd75ab3cd7" dependencies = [ - "darling", + "darling 0.20.10", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -2845,6 +3220,17 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +[[package]] +name = "postcard" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55c51ee6c0db07e68448e336cf8ea4131a620edefebf9893e759b2d793420f8" +dependencies = [ + "cobs", + "embedded-io", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2891,20 +3277,30 @@ dependencies = [ [[package]] name = "primal-check" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" +checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" dependencies = [ "num-integer", ] +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + [[package]] name = "proc-macro-crate" version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit", + "toml_edit 0.21.1", ] [[package]] @@ -2932,9 +3328,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -2945,7 +3341,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "731e0d9356b0c25f16f33b5be79b1c57b562f141ebfcdb0ad8ac2c13a24293b4" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", "lazy_static", "procfs-core", @@ -2958,7 +3354,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "hex", ] @@ -3027,11 +3423,58 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "memchr", "unicase", ] +[[package]] +name = "quinn" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ceeeeabace7857413798eb1ffa1e9c905a9946a57d81fb69b4b71c4d8eb3ad" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.11", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf517c03a109db8100448a4be38d498df8a210a99fe0e1b9eaf39e78c640efe" +dependencies = [ + "bytes", + "rand", + "ring 0.17.8", + "rustc-hash", + "rustls 0.23.11", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +dependencies = [ + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quote" version = "1.0.36" @@ -3116,23 +3559,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.4" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", ] [[package]] @@ -3146,13 +3589,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.4", ] [[package]] @@ -3163,17 +3606,17 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "regress" -version = "0.9.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eae2a1ebfecc58aff952ef8ccd364329abe627762f5bf09ff42eb9d98522479" +checksum = "82a9ecfa0cb04d0b04dddb99b8ccf4f66bc8dfd23df694b398570bd8ae3a50fb" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.13.2", "memchr", ] @@ -3188,22 +3631,24 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" dependencies = [ "async-compression", "base64 0.22.1", "bytes", "cookie", "cookie_store", + "encoding_rs", "futures-channel", "futures-core", "futures-util", + "h2 0.4.5", "http 1.1.0", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.4.1", "hyper-rustls", "hyper-util", "ipnet", @@ -3214,16 +3659,18 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls 0.22.4", - "rustls-native-certs 0.7.0", + "quinn", + "rustls 0.23.11", + "rustls-native-certs 0.7.1", "rustls-pemfile 2.1.2", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls 0.26.0", "tokio-socks", "tokio-util", "tower-service", @@ -3232,7 +3679,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 0.26.1", + "webpki-roots 0.26.3", "winreg", ] @@ -3254,9 +3701,9 @@ dependencies = [ [[package]] name = "reqwest-middleware" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45d100244a467870f6cb763c4484d010a6bed6bd610b3676e3825d93fb4cfbd" +checksum = "39346a33ddfe6be00cbc17a34ce996818b97b230b87229f10114693becca1268" dependencies = [ "anyhow", "async-trait", @@ -3269,9 +3716,9 @@ dependencies = [ [[package]] name = "reqwest-retry" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40f342894422862af74c50e1e9601cf0931accc9c6981e5eb413c46603b616b5" +checksum = "cf2a94ba69ceb30c42079a137e2793d6d0f62e581a24c06cd4e9bb32e973c7da" dependencies = [ "anyhow", "async-trait", @@ -3279,7 +3726,7 @@ dependencies = [ "futures", "getrandom", "http 1.1.0", - "hyper", + "hyper 1.4.1", "parking_lot 0.11.2", "reqwest", "reqwest-middleware", @@ -3291,12 +3738,10 @@ dependencies = [ [[package]] name = "retry-policies" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "493b4243e32d6eedd29f9a398896e35c6943a123b55eec97dcaee98310d25810" +checksum = "5875471e6cab2871bc150ecb8c727db5113c9338cc3354dc5ee3425b6aa40a1c" dependencies = [ - "anyhow", - "chrono", "rand", ] @@ -3332,9 +3777,9 @@ dependencies = [ [[package]] name = "ringbuf" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79abed428d1fd2a128201cec72c5f6938e2da607c6f3745f769fabea399d950a" +checksum = "5c65e4c865bc3d2e3294493dff0acf7e6c259d066e34e22059fa9c39645c3636" dependencies = [ "crossbeam-utils", ] @@ -3390,9 +3835,9 @@ dependencies = [ [[package]] name = "rspotify" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efe9fecaed050e72eefa9a07702c3734abb0e82b70d7c867b32789e6f8fb5663" +checksum = "0b2487556b568b6471cbbdcca0d23e1eff885e993cf057254813dc46c3837922" dependencies = [ "async-stream", "async-trait", @@ -3414,9 +3859,9 @@ dependencies = [ [[package]] name = "rspotify-http" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612220388516c12ab0fc23917dd02a6693488dbb5ac615436ff60e7e88130ec1" +checksum = "9424544350d142db73534967503205030162c4e650b2f17fe1729f5dc5c8edff" dependencies = [ "async-trait", "log", @@ -3428,21 +3873,21 @@ dependencies = [ [[package]] name = "rspotify-macros" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6396935068e8615651966a7e44f1dbcfab7be02544862fb3ae65f1e086ad9a93" +checksum = "475cd14f84b46cc8d96389e0c892940cd2bd32a9240d1ba6fefcdfd8432ab9d1" [[package]] name = "rspotify-model" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f104ee213b5cb53e6d797efc7e1825bb42981213265a2db7268efd51ecaff" +checksum = "496322604d8dfe61c10a37a88bb4ecac955e2d66fdbe148b2c44a844048b1ca2" dependencies = [ "chrono", "enum_dispatch", "serde", "serde_json", - "strum", + "strum 0.26.3", "thiserror", ] @@ -3516,7 +3961,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -3555,7 +4000,21 @@ dependencies = [ "log", "ring 0.17.8", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.5", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls" +version = "0.23.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" +dependencies = [ + "once_cell", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.5", "subtle", "zeroize", ] @@ -3574,9 +4033,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "a88d6d420651b496bdd98684116959239430022a115c1240e6c3993be0b15fba" dependencies = [ "openssl-probe", "rustls-pemfile 2.1.2", @@ -3622,9 +4081,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -3652,13 +4111,12 @@ dependencies = [ [[package]] name = "rusty_ytdl" -version = "0.7.2" -source = "git+https://github.com/cycle-five/rusty_ytdl?branch=v0.7.2-boa#4584779547956725b68ab0b9abbcd4e46324ab92" +version = "0.7.3" +source = "git+https://github.com/cycle-five/rusty_ytdl?branch=main#c0175408ee26b8648e46c8a87eafb297920f1d8d" dependencies = [ "aes", "async-trait", "boa_engine", - "boa_parser", "bytes", "cbc", "derivative", @@ -3690,9 +4148,9 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "ryu-js" -version = "1.0.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad97d4ce1560a5e27cec89519dc8300d1aa6035b099821261c651486a19e44d5" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" [[package]] name = "salsa20" @@ -3777,11 +4235,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3790,9 +4248,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3804,7 +4262,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cssparser", "derive_more", "fxhash", @@ -3828,9 +4286,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] @@ -3852,7 +4310,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.1", "serde", ] @@ -3867,20 +4325,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -3906,7 +4364,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -3921,48 +4379,18 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.2.6", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.66", -] - [[package]] name = "serenity" -version = "0.12.1" -source = "git+https://github.com/CycleFive/serenity?branch=current#b2e932cd2985bcc90242ede5b1435b4f40802cab" +version = "0.12.2" +source = "git+https://github.com/CycleFive/serenity?branch=current#742a0ceeb51f8b364db01a3c71a6284add524ac5" dependencies = [ "arrayvec", "async-trait", "base64 0.22.1", - "bitflags 2.5.0", + "bitflags 2.6.0", "bytes", "chrono", - "dashmap", + "dashmap 5.5.3", "flate2", "futures", "fxhash", @@ -3977,7 +4405,7 @@ dependencies = [ "serde_json", "time", "tokio", - "tokio-tungstenite 0.21.0", + "tokio-tungstenite 0.23.1", "tracing", "typemap_rev", "typesize", @@ -3990,7 +4418,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "593682f6155d07c8b331b3d1060f5aab7e6796caca9f2f66bd9e6855c880e06b" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "num-traits", "serde", "serde_json", @@ -4037,6 +4465,18 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shorthand" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474f77f985d8212610f170332eaf173e768404c0c1d4deb041f32c297cf18931" +dependencies = [ + "from_map", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "signal-hook-registry" version = "1.4.2" @@ -4075,7 +4515,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16d23b015676c90a0f01c197bfdc786c20342c73a0afdda9025adb0bc42940a8" dependencies = [ "bytecount", - "cargo_metadata", + "cargo_metadata 0.14.2", "error-chain", "glob", "pulldown-cmark", @@ -4110,15 +4550,15 @@ dependencies = [ [[package]] name = "songbird" -version = "0.4.1" -source = "git+https://github.com/cycle-five/songbird?branch=current#75c42b2b4243f6429e7e0116008628339002e457" +version = "0.4.2" +source = "git+https://github.com/cycle-five/songbird?branch=current#7277997b348d78d319a05eee48f814598168f4a4" dependencies = [ "async-trait", "audiopus", "byteorder", "bytes", "crypto_secretbox", - "dashmap", + "dashmap 5.5.3", "derivative", "discortp", "flume", @@ -4138,11 +4578,12 @@ dependencies = [ "serenity", "serenity-voice-model", "socket2", + "stream_lib", "streamcatcher", "symphonia", "symphonia-core", "tokio", - "tokio-tungstenite 0.21.0", + "tokio-tungstenite 0.23.1", "tokio-util", "tracing", "tracing-futures", @@ -4185,11 +4626,10 @@ checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" [[package]] name = "sqlformat" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce81b7bd7c4493975347ef60d8c7e8b742d4694f4c49f93e0a12ea263938176c" +checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" dependencies = [ - "itertools", "nom", "unicode_categories", ] @@ -4229,7 +4669,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.2.6", + "indexmap", "log", "memchr", "once_cell", @@ -4272,7 +4712,7 @@ checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", - "heck", + "heck 0.4.1", "hex", "once_cell", "proc-macro2", @@ -4298,7 +4738,7 @@ checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "bytes", "chrono", @@ -4342,7 +4782,7 @@ checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", "base64 0.21.7", - "bitflags 2.5.0", + "bitflags 2.6.0", "byteorder", "chrono", "crc", @@ -4399,6 +4839,15 @@ dependencies = [ "urlencoding", ] +[[package]] +name = "stable-vec" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1dff32a2ce087283bec878419027cebd888760d8760b2941ad0843531dc9ec8" +dependencies = [ + "no-std-compat", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -4411,6 +4860,23 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "stream_lib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e106dd009a0dfd2cf57777c39cad08f852debd366df6e841b250d956cec3277e" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "hls_m3u8", + "patricia_tree", + "reqwest", + "tokio", + "tracing", + "url", +] + [[package]] name = "streamcatcher" version = "1.0.1" @@ -4465,6 +4931,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "strsim" version = "0.11.1" @@ -4473,31 +4945,52 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "strum" -version = "0.26.2" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "530efb820d53b712f4e347916c5e7ed20deb76a4f0457943b3182fb889b06d2c" +dependencies = [ + "strum_macros 0.17.1", +] + +[[package]] +name = "strum" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" dependencies = [ - "strum_macros", + "strum_macros 0.26.4", ] [[package]] name = "strum_macros" -version = "0.26.2" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6e163a520367c465f59e0a61a23cfae3b10b6546d78b6f672a382be79f7110" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", "rustversion", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" @@ -4514,6 +5007,7 @@ dependencies = [ "symphonia-codec-pcm", "symphonia-codec-vorbis", "symphonia-core", + "symphonia-format-caf", "symphonia-format-isomp4", "symphonia-format-mkv", "symphonia-format-ogg", @@ -4610,6 +5104,17 @@ dependencies = [ "log", ] +[[package]] +name = "symphonia-format-caf" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e43c99c696a388295a29fe71b133079f5d8b18041cf734c5459c35ad9097af50" +dependencies = [ + "log", + "symphonia-core", + "symphonia-metadata", +] + [[package]] name = "symphonia-format-isomp4" version = "0.5.4" @@ -4695,9 +5200,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "b146dcf730474b4bcd16c311627b31ede9ab149045db4d6088b3becaea046462" dependencies = [ "proc-macro2", "quote", @@ -4713,14 +5218,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" [[package]] name = "synstructure" @@ -4730,7 +5235,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -4743,6 +5248,41 @@ dependencies = [ "libc", ] +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -4792,22 +5332,22 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -4828,7 +5368,6 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", - "js-sys", "libc", "num-conv", "num_threads", @@ -4856,19 +5395,20 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "8faa444297615a4e020acb64146b0603c9c395c03a97c17fd9028816d3b4d63e" dependencies = [ "displaydoc", + "serde", "zerovec", ] [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -4881,16 +5421,15 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.37.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", "libc", "mio", "num_cpus", - "parking_lot 0.12.3", "pin-project-lite", "signal-hook-registry", "socket2", @@ -4900,13 +5439,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -4931,6 +5470,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.11", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-socks" version = "0.5.1" @@ -4978,12 +5528,24 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.4", - "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", "tungstenite 0.21.0", - "webpki-roots 0.26.1", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.11", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tungstenite 0.23.0", + "webpki-roots 0.26.3", ] [[package]] @@ -5014,13 +5576,24 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "toml_edit" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.6", + "indexmap", "toml_datetime", "winnow", ] @@ -5064,18 +5637,6 @@ dependencies = [ "tracing-core", ] -[[package]] -name = "tracing-appender" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" -dependencies = [ - "crossbeam-channel", - "thiserror", - "time", - "tracing-subscriber", -] - [[package]] name = "tracing-attributes" version = "0.1.27" @@ -5084,7 +5645,25 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash 0.8.11", + "gethostname", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", ] [[package]] @@ -5107,6 +5686,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -5133,7 +5723,7 @@ dependencies = [ "thread_local", "tracing", "tracing-core", - "tracing-log", + "tracing-log 0.2.0", ] [[package]] @@ -5154,9 +5744,9 @@ checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc" [[package]] name = "triomphe" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" [[package]] name = "trust-dns-client" @@ -5251,14 +5841,32 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.22.4", - "rustls-pki-types", "sha1", "thiserror", "url", "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.23.11", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "twilight-gateway" version = "0.15.4" @@ -5321,7 +5929,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb704842c709bc76f63e99e704cb208beeccca2abbabd0d9aec02e48ca1cee0f" dependencies = [ "chrono", - "dashmap", + "dashmap 5.5.3", "hashbrown 0.14.5", "mini-moka", "parking_lot 0.12.3", @@ -5340,7 +5948,7 @@ checksum = "905e88c2a4cc27686bd57e495121d451f027e441388a67f773be729ad4be1ea8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -5387,9 +5995,9 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "unicode-xid" @@ -5427,9 +6035,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna 0.5.0", @@ -5469,9 +6077,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] @@ -5501,7 +6109,7 @@ dependencies = [ "enum-ordinalize", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -5516,6 +6124,21 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "8.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566" +dependencies = [ + "anyhow", + "cargo_metadata 0.18.1", + "cfg-if", + "regex", + "rustversion", + "sysinfo", + "time", +] + [[package]] name = "version_check" version = "0.9.4" @@ -5541,6 +6164,37 @@ dependencies = [ "try-lock", ] +[[package]] +name = "warp" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "headers", + "http 0.2.12", + "hyper 0.14.30", + "log", + "mime", + "mime_guess", + "multer", + "percent-encoding", + "pin-project", + "rustls-pemfile 2.1.2", + "scoped-tls", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls 0.25.0", + "tokio-tungstenite 0.21.0", + "tokio-util", + "tower-service", + "tracing", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5574,7 +6228,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "wasm-bindgen-shared", ] @@ -5608,7 +6262,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5675,9 +6329,9 @@ checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" [[package]] name = "webpki-roots" -version = "0.26.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" dependencies = [ "rustls-pki-types", ] @@ -5745,13 +6399,23 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -5769,7 +6433,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -5789,18 +6453,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -5811,9 +6475,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -5823,9 +6487,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -5835,15 +6499,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -5853,9 +6517,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -5865,9 +6529,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -5877,9 +6541,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -5889,9 +6553,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" @@ -5935,9 +6599,9 @@ dependencies = [ [[package]] name = "xml5ever" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c376f76ed09df711203e20c3ef5ce556f0166fa03d39590016c0fd625437fad" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", "mac", @@ -5964,28 +6628,28 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "synstructure", ] [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] [[package]] @@ -6005,7 +6669,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", "synstructure", ] @@ -6017,10 +6681,11 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" -version = "0.10.2" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +checksum = "432bfb1b38809863a16add25daeff2cc63c8e6bbc1cb05b178237e35ab457885" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", @@ -6028,11 +6693,11 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.2" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +checksum = "fa94b6a91d81a9d96473412885b87d8fb677accc447cae54571f93313aebf109" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.71", ] diff --git a/Cargo.toml b/Cargo.toml index 7b828deff..c81e73fd3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,12 @@ [workspace] -members = ["cracktunes", "crack-core", "crack-osint", "crack-gpt"] +members = [ + "cracktunes", + "crack-core", + "crack-osint", + "crack-gpt", + "crack-bf", + "crack-voting", +] resolver = "2" [workspace.package] @@ -7,20 +14,6 @@ edition = "2021" license = "MIT" authors = ["Cycle Five "] -[workspace.dependencies] -crack-core = { path = "crack-core", default-features = true, version = "0.3.7" } -crack-osint = { path = "crack-osint", default-features = true, version = "0.1" } -crack-gpt = { path = "crack-gpt", default-features = true, version = "0.1" } - -reqwest = { version = "0.12.4", default-features = false, features = [ - "blocking", - "json", - "multipart", - "rustls-tls", -] } -tracing = "0.1.40" - - [workspace.dependencies.sqlx] version = "0.7.4" default-features = false @@ -30,13 +23,15 @@ features = [ "macros", "postgres", "chrono", + "time", "migrate", "json", ] [workspace.dependencies.serenity] -version = "0.12.1" +# Broken? try self-host. git = "https://github.com/CycleFive/serenity" +version = "0.12" branch = "current" default-features = false features = [ @@ -56,21 +51,62 @@ features = [ ] [workspace.dependencies.songbird] +# Broken? try self-hosted? git = "https://github.com/cycle-five/songbird" branch = "current" -version = "0.4.1" +version = "0.4.2" features = ["driver", "serenity", "rustls", "receive", "builtin-queue"] -[workspace.dependencies.poise] -git = "https://github.com/cycle-five/poise" -branch = "current" -default-features = true -features = ["cache", "chrono"] +[workspace.dependencies.symphonia] +version = "0.5.4" +features = ["all-formats", "all-codecs"] [workspace.dependencies.tokio] -version = "1.37.0" +version = "1.38" default-features = false -features = ["macros", "rt", "rt-multi-thread", "signal", "sync"] +features = ["macros", "rt", "rt-multi-thread", "signal", "sync", "io-util"] + +[workspace.dependencies] +crack-core = { path = "./crack-core", default-features = true, version = "0.3" } +crack-osint = { path = "./crack-osint", default-features = true, version = "0.1" } +crack-gpt = { path = "../crack-gpt", default-features = true, version = "0.2" } +crack-bf = { path = "../crack-bf", default-features = true, version = "0.1" } +poise = { branch = "current", git = "https://github.com/cycle-five/poise", default-features = true } +vergen = { version = "9", features = ["git", "cargo", "si", "build", "gitcl"] } +tracing = "0.1" +reqwest = { version = "0.12", default-features = false, features = [ + "blocking", + "json", + "multipart", + "rustls-tls", + "cookies", + "charset", + "http2", + "macos-system-configuration", +] } + +# Config for 'cargo dist' +[workspace.metadata.dist] +# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) +cargo-dist-version = "0.17.0" +# CI backends to support +ci = "github" +# The installers to generate for each app +installers = ["shell", "npm"] +# Target platforms to build apps for (Rust target-triple syntax) +targets = ["x86_64-unknown-linux-gnu"] +# Publish jobs to run in CI +pr-run-mode = "upload" +# Whether to install an updater program +install-updater = false +# Where to host releases +hosting = "github" +# The archive format to use for non-windows builds (defaults .tar.xz) +unix-archive = ".tar.gz" +# A namespace to use when publishing this package to the npm registry +npm-scope = "@cracktunes" +# The archive format to use for windows builds (defaults .zip) +windows-archive = ".tar.gz" [profile.release] incremental = true @@ -85,3 +121,8 @@ debug = 1 inherits = "release" lto = true opt-level = 3 + +# The profile that 'cargo dist' will build with +[profile.dist] +inherits = "release-with-performance" +lto = "thin" diff --git a/Dockerfile b/Dockerfile index 13124096d..7b3884619 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,29 @@ # Build image # Necessary dependencies to build CrackTunes -FROM debian:bookworm-slim as build +FROM debian:bookworm-slim AS build ARG SQLX_OFFLINE=true RUN apt-get update && apt-get install -y \ - autoconf \ - automake \ - cmake \ - libtool \ - libssl-dev \ - pkg-config \ - libopus-dev \ - curl \ - git + autoconf \ + automake \ + cmake \ + libtool \ + libssl-dev \ + pkg-config \ + libopus-dev \ + curl \ + git # Get Rust RUN curl -proto '=https' -tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ - && . "$HOME/.cargo/env" \ - && rustup default stable + && . "$HOME/.cargo/env" \ + && rustup default nightly WORKDIR "/app" COPY . . COPY names.txt /app/names.txt -RUN . "$HOME/.cargo/env" && cargo build --release --locked +RUN . "$HOME/.cargo/env" && cargo build --release --locked --features crack-bf,crack-gpt,crack-osint # Release image # Necessary dependencies to run CrackTunes @@ -40,29 +40,26 @@ RUN mkdir -p /data && chown -R ${USER_UID}:${USER_GID} /data # Update the package list, install sudo, create a non-root user, and grant password-less sudo permissions RUN groupadd --gid $USER_GID $USERNAME \ - && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ - # - # [Optional] Add sudo support. Omit if you don't need to install software after connecting. - && apt-get update \ - && apt-get install -y sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ - && chmod 0440 /etc/sudoers.d/$USERNAME + && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \ + # + # [Optional] Add sudo support. Omit if you don't need to install software after connecting. + && apt-get update \ + && apt-get install -y sudo \ + && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME -USER $USERNAME - -RUN sudo apt-get update \ - # && apt-get upgrade -y \ - && sudo apt-get install -y ffmpeg curl \ - # Clean up - && sudo apt-get autoremove -y \ - && sudo apt-get clean -y \ - && sudo rm -rf /var/lib/apt/lists/* -#RUN sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/download/2024.04.09/yt-dlp_linux \ -RUN sudo curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp-nightly-builds/releases/download/2024.05.11.232654/yt-dlp_linux \ - && sudo chmod +x /usr/local/bin/yt-dlp +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y ffmpeg curl \ + && apt-get autoremove -y \ + && apt-get clean -y \ + && rm -rf /var/lib/apt/lists/* +RUN curl -sSL --output /usr/local/bin/yt-dlp https://github.com/yt-dlp/yt-dlp/releases/download/2024.07.09/yt-dlp_linux \ + && chmod +x /usr/local/bin/yt-dlp +USER $USERNAME RUN yt-dlp -v -h @@ -76,7 +73,7 @@ COPY --chown=${USER_UID}:${USER_GID} --from=build /app/cracktunes.toml $HOME/app COPY --chown=${USER_UID}:${USER_GID} --from=build /app/names.txt $HOME/app/names.txt # RUN ls -al / && ls -al /data -ENV APP_ENVIRONMENT production RUN . "$HOME/app/.env" -ENV DATABASE_URL postgresql://postgres:mysecretpassword@localhost:5432/postgres +ENV APP_ENVIRONMENT=production +ENV DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres CMD ["/home/cyclefive/app/cracktunes"] diff --git a/README.md b/README.md index 695d9024c..c9ed17cfd 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![CrackTunes](./docs/logo.png) - A hassle-free, highly performant, host-it-yourself, cracking smoking Discord music bot +A hassle-free, highly performant, host-it-yourself, cracking smoking Discord music bot [![builds.sr.ht status](https://builds.sr.ht/~cycle-five.svg)](https://builds.sr.ht/~cycle-five?) [![GitHub CI workflow status](https://github.com/cycle-five/cracktunes/actions/workflows/ci_workflow.yml/badge.svg)](https://github.com/cycle-five/cracktunes/actions/workflows/ci_workflow.yml) @@ -10,19 +10,19 @@ ## Aknowledgements -Thanks to the guys over at [alwaysdata](https://www.alwaysdata.com/) for hosting the website, web portal, email, etc for this project for free, in their [Open Source](https://www.alwaysdata.com/en/open-source/) program. +Thanks to the guys over at [alwaysdata](https://www.alwaysdata.com/) for hosting the website, web portal, email, etc for this project for free, in their [Open Source](https://www.alwaysdata.com/en/open-source/) program. ## Deployment ### Usage -* Create a bot account -* Copy the **token** and **application id** to a `.env` with the `DISCORD_TOKEN` and `DISCORD_APP_ID` environment variables respectively. -* Define `DATABASE_URL`, `PG_USER`, `PG_PASSWORD` for the Postgres database. -* *Optional* define `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` for Spotify support. -* *Optional* define `OPENAI_API_KEY` for chatgpt support. -* *Optional* define `VIRUSTOTAL_API_KEY` for osint URL checking. -* Use [.env.example](https://github.com/cycle-five/cracktunes/blob/master/.env.example) as a starting point. +- Create a bot account +- Copy the **token** and **application id** to a `.env` with the `DISCORD_TOKEN` and `DISCORD_APP_ID` environment variables respectively. +- Define `DATABASE_URL`, `PG_USER`, `PG_PASSWORD` for the Postgres database. +- _Optional_ define `SPOTIFY_CLIENT_ID` and `SPOTIFY_CLIENT_SECRET` for Spotify support. +- _Optional_ define `OPENAI_API_KEY` for chatgpt support. +- _Optional_ define `VIRUSTOTAL_API_KEY` for osint URL checking. +- Use [.env.example](https://github.com/cycle-five/cracktunes/blob/master/.env.example) as a starting point. ### Docker **FIXME** @@ -73,7 +73,7 @@ apt install -y pkg-config The following command will run all tests: ```shell -cargo +nightly test --all +cargo +nightly test --all-features --workspace ``` Some tests are available inside the `src/tests` folder, others are in their respective @@ -82,7 +82,39 @@ Increasing the test coverage is also welcome. Test coverage is tracked using [tarpaulin](). ```shell -cargo +nightly tarpaulin --all +cargo +nightly tarpaulin --all-features --workspace +``` + +## Linting + +```shell +cargo +nightly clippy --profile=release --all-features --workspace -- -D warnings -D clippy:all +``` + +## Build + +```shell +cargo +nightly build --profile=release --features crack-osint,crack-bf,crack-fpt --workspace --locked +``` + +## Distribution + +```shell +cargo dist init --hosting github +# make change `pr-run-mode = "upload"` +git add . +git commit -am "chore: cargo-dist" +cargo dist build --profile=release --features crack-gpt,crack-bf,crack-osint +``` + +## Release + +```shell +git tag vX.X.X +git push --tags + +# publish to crates.io (optional) +cargo publish ``` ### Docker Compose @@ -94,64 +126,104 @@ docker build -t cracktunes . docker compose up -d ``` +# Change Log + +## TODO: + +- [ ] discordbotlist.com (voting service) +- [ ] Finish adding help option to all commands +- [ ] Update and make wider use of rusty_ytdlp. + +## v0.3.8 (2024/07/17) + +- [x] Looked at rolling back to reqwest 2.11 because it was causing problems. + Decided to stick with 2.12 and keep using the forked and patched version + of serenity, poise, songbird, etc. +- [x] Pulled in songbird update to support soundcloud and streaming m8u3 files. +- [x] More refactoring. +- [x] Brainf\*\*k interpreter. +- [x] Switched all locks from blocking to non-blocking async. +- [x] Unify messaging module. +- [x] Fixed repeat bug when nothing is playing. +- [-] Change `let _ = send_reply(&ctx, msg, true).await?;` + to `ctx.send_reply(msg, true).await?;` (half done) + ... +For next version... -# ~~Roadmap~~ Change Log ## v0.3.7 (2024/05/29) -- [x] crackgpt 0.2.0! - Added back chatgpt support, which I am now self hosting for CrackTunes - and is backed by GPT 4o. -- [x] Use the rusty_ytdl library as a first try, fallback to yt-dlp if it fails. -- [x] Remove the grafana dashboard. -- [x] Switch to async logging. -- [x] Add an async service to handle the database (accept writes on a channel, - and write to the database in a separate thread). - Eventually this could be a seperate service (REST / GRPC). + +- crackgpt 0.2.0! + Added back chatgpt support, which I am now self hosting for CrackTunes + and is backed by GPT 4o. +- Use the rusty_ytdl library as a first try, fallback to yt-dlp if it fails. +- Remove the grafana dashboard. +- Switch to async logging. +- Add an async service to handle the database (accept writes on a channel, + and write to the database in a separate thread). + Eventually this could be a seperate service (REST / GRPC). + ## v0.3.6 (2024/05/03) + - Music channel setting (can lock music playing command and responses to a specific channel) - Fixes in logging - Fixes in admin commands - Lots of refactoring code cleanup. + ## v0.3.5 (2024/04/23) + - Significantly improved loading speed of songs into the queue. - Fix Youtube Playlists. - Lots of refactoring. - Can load spotify playlists very quickly - Option to vote for Crack Tunes on top.gg for 12 hours of premium access. + ## v0.3.4 + - playlist loadspotify and playlist play commands - Invite and voting links - Updated serenity / poise / songbird to latest versions - Refactored functions for creating embeds and sending messages to it's own module ## v0.3.3 (2024/04/??) + - `/loadspotify ` loads a spotify playlist into a Crack Tunes playlist. - voting tracking ## v0.3.2 (2024/03/27) + - Playlists! - Here are the available playlist commands - - `/playlist create ` Creates a playlist with the given name - - `/playlist delete ` Deletes a playlist with the given name - - `/playlist addto ` Adds the currently playing song to - - `/playlist list` List your playlists - - `/playlist get ` displays the contents of - - `/playlist play ` queues the given playlist on the bot + - `/playlist create ` Creates a playlist with the given name + - `/playlist delete ` Deletes a playlist with the given name + - `/playlist addto ` Adds the currently playing song to + - `/playlist list` List your playlists + - `/playlist get ` displays the contents of + - `/playlist play ` queues the given playlist on the bot - Added pl alias for playlist - Added /playlist list - Fixed Requested by Field - JSON for grafana dashboards + ## v0.3.1 (2024/03/21) + - Fix the requesting user not always displaying - Reversed order of this Change Log so newest stuff is on top + ## ~~0.3.0-rc.6~~ + ## 0.3.0 + - Added more breakdown of features which can be optionally turned on/off - Telemitry - Metrics / logging -- Removed a lot of unescesarry dependencies +- Removed a lot of unescesarry dependencies + ## 0.1.4 (crack-osint) (2024/03/12) + - osint scan command to check urls for malicious content + ## 0.3.0-rc.5 (2024/03/09) + - cargo update - GuildId checks - user authorized message @@ -159,49 +231,71 @@ docker compose up -d - add feature for osint - make admin commands usable by guild members with admin - add dry run to rename_all + ## 0.3.0-rc.4 -* fix storing auto role and timeout I think -* download and skip together -* ~~try to finally fix this fucking volume bug~~ -* fix loading guild settings -* add pgadmin to docker compose -* ~~fix volume~~ (volume is still broken) + +- fix storing auto role and timeout I think +- download and skip together +- ~~try to finally fix this fucking volume bug~~ +- fix loading guild settings +- add pgadmin to docker compose +- ~~fix volume~~ (volume is still broken) + ## 0.3.0-rc.2 + - [x] Clean command - [x] Bug fixes - ~~[ ] Down vote~~ (not working) + ## 0.3.0-rc.1 + - [x] Dockerized! - [x] Refactored settings commands. - [x] Storing and retrieving settings from Postgres. - [x] Updated dependencies to be in line with current. + ## ~~0.2.13~~ + - ~~[] Port to next branch of serenity~~ - ~~[] Flesh out admin commands~~ + ## ~~0.2.12~~ + ## ~~0.2.6~~ + Didn't really track stuff here... + ## 0.2.5 + - ~~[] Shuttle~~ - ~~[] Reminders~~ - ~~[] Notes~~ + ## 0.2.4 (2023/07/17) + - [x] Bug fixes. - [x] Remove reliance on slash commands everywhere. - [x] Remove shuttle for now + ## 0.2.3 + - [x] Bug fixes (volume) - [x] Shuttle support (still broken) + ## 0.2.2 (2023/07/09 ish) + - [x] Welcome Actions - [x] Play on multiple servers at once + ## 0.2.1 (2023/07/02) + - [x] Play music from local files + ## 0.2.0 + - [x] Play music from YouTube - [x] Play music from Spotify (kind of...) -

Originally forked from Parrot -

\ No newline at end of file +

diff --git a/build.rs b/build.rs index e4aa65708..6cc17f368 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,13 @@ -fn main() { +fn main() -> Result<(), Box> { + // For the githash of the build + vergen::EmitBuilder::builder() + .all_build() + .all_git() + .emit()?; // make sure tarpaulin is included in the build - println!("cargo::rustc-check-cfg=cfg(tarpaulin_include)"); + println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)"); // generated by `sqlx migrate build-script` // trigger recompilation when a new migration is added println!("cargo:rerun-if-changed=migrations"); + Ok(()) } diff --git a/crack-bf/Cargo.toml b/crack-bf/Cargo.toml new file mode 100644 index 000000000..17cf97862 --- /dev/null +++ b/crack-bf/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "crack-bf" +version = "0.1.0" +edition = "2021" +authors = ["Cycle Five "] +publish = true +license = "MIT" +description = "Brainfuck interpreter for cracktunes." +keywords = [ + "music", + "discord", + "bot", + "crack", + "tunes", + "brainfuck", + "programming language", +] +categories = ["multimedia::audio"] +homepage = "https://cracktun.es/" +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +#repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +workspace = "../" + + +[dependencies] +tokio = { workspace = true } diff --git a/crack-bf/src/lib.rs b/crack-bf/src/lib.rs new file mode 100644 index 000000000..140979736 --- /dev/null +++ b/crack-bf/src/lib.rs @@ -0,0 +1,289 @@ +use std::io::{BufRead, Write}; +use tokio::io::{ + AsyncBufRead, AsyncWrite, {AsyncBufReadExt, AsyncWriteExt}, +}; +pub type Error = Box; + +/// Representation of a brainfuck program. +#[derive(Clone, Debug)] +pub struct BrainfuckProgram { + pub code: Vec, + pub cells: [u8; 30000], + pub ptr: usize, + pub pc: usize, +} + +/// Implementation of the representation and execution of a brainfuck program. +impl BrainfuckProgram { + pub fn new(program: String) -> Self { + let program = program + .chars() + .filter(|c| matches!(c, '+' | '-' | '<' | '>' | '[' | ']' | '.' | ',')) + .collect::(); + let code = program.as_bytes(); + + Self { + code: code.to_vec(), + cells: [0u8; 30000], + ptr: 0, + pc: 0, + } + } + + /// Match a bracket b'[' to the matching b']' on the same level of + /// precedence. Returns the new PC pointer. + /// TODO: Add some error handling. + fn match_bracket_forward(cells: &[u8], ptr: usize, code: &[u8], pc: usize) -> usize { + let mut pc = pc; + if cells[ptr] == 0 { + let mut depth = 1; + while depth > 0 { + pc += 1; + if code[pc] == b'[' { + depth += 1; + } else if code[pc] == b']' { + depth -= 1; + } + } + } + pc + } + + /// Match a bracket b']' to the matching b'[' on the same level of + /// precedence. Returns the new PC pointer. + /// TODO: Add some error handling. + fn match_bracket_backward(cells: &[u8], ptr: usize, code: &[u8], pc: usize) -> usize { + let mut pc = pc; + if cells[ptr] != 0 { + let mut depth = 1; + while depth > 0 { + pc -= 1; + if code[pc] == b'[' { + depth -= 1; + } else if code[pc] == b']' { + depth += 1; + } + } + } + pc + } + + /// Write the current cell to the writer. + #[allow(dead_code)] + fn write_cell(&self, writer: &mut impl Write) -> Result<(), Error> { + let val = self.cells[self.ptr]; + match writer.write_all(&[val]) { + Ok(_) => Ok(()), + Err(e) => Err(Box::new(e)), + } + } + + // async fn write_cell_async(&self, writer: &mut impl AsyncWrite) -> Result<(), Error> { + // let val = self.cells[self.ptr]; + // match w { + // Ok(_) => Ok(()), + // Err(e) => Err(Box::new(e)), + // } + // } + + /// Run the brainfuck program. + pub async fn run_async(&mut self, mut reader: R, mut writer: W) -> Result + where + R: AsyncBufRead + Unpin, + W: AsyncWrite + Unpin, + { + let mut bytes_wrote = 0; + let mut cells = self.cells; + let code = &self.code.clone(); + let mut pc = 0; + let mut ptr = 0; + while pc < code.len() { + match code[pc] { + b'+' => cells[ptr] = cells[ptr].wrapping_add(1), + b'-' => cells[ptr] = cells[ptr].wrapping_sub(1), + b'<' => ptr = ptr.wrapping_sub(1), + b'>' => ptr = ptr.wrapping_add(1), + b'[' => pc = Self::match_bracket_forward(&cells, ptr, code, pc), + b']' => pc = Self::match_bracket_backward(&cells, ptr, code, pc), + b'.' => { + let val = cells[ptr]; + match writer.write(&[val]).await { + Ok(_) => { + bytes_wrote += 1; + }, + Err(e) => { + writer.flush().await?; + return Err(Box::new(e)); + }, + } + }, + b',' => match reader.fill_buf().await { + Ok(buf) => { + if buf.is_empty() { + cells[ptr] = 0; + } else { + cells[ptr] = buf[0]; + reader.consume(1); + } + }, + Err(_) => { + cells[ptr] = 0; + }, + }, + _ => {}, + } + pc += 1; + } + Ok(bytes_wrote) + } + + /// Run the brainfuck program. + pub fn run(&mut self, mut reader: R, mut writer: W) -> Result<(), Error> + where + R: BufRead, + W: Write, + { + let mut cells = self.cells; + let code = &self.code.clone(); + let mut pc = 0; + let mut ptr = 0; + while pc < code.len() { + match code[pc] { + b'+' => cells[ptr] = cells[ptr].wrapping_add(1), + b'-' => cells[ptr] = cells[ptr].wrapping_sub(1), + b'<' => ptr = ptr.wrapping_sub(1), + b'>' => ptr = ptr.wrapping_add(1), + b'[' => pc = Self::match_bracket_forward(&cells, ptr, code, pc), + b']' => pc = Self::match_bracket_backward(&cells, ptr, code, pc), + b'.' => { + let val = cells[ptr]; + match writer.write_all(&[val]) { + Ok(_) => {}, + Err(e) => { + // self.done.store(true, Ordering::Relaxed); + return Err(Box::new(e)); + // self + }, + } + }, + b',' => { + let mut input = [0u8; 1]; + match reader.read_exact(&mut input) { + Ok(_) => { + cells[ptr] = input[0]; + }, + Err(_) => { + cells[ptr] = 0; + }, + } + }, + _ => {}, + } + pc += 1; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use std::io::BufReader; + use std::io::Cursor; + + #[test] + fn test_hello_world_cursor() { + let program = String::from("++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."); + let mut bf = BrainfuckProgram::new(program); + + let input = Cursor::new(vec![]); + let mut output = Cursor::new(vec![]); + + bf.run(input, &mut output).unwrap(); + + let result = String::from_utf8(output.into_inner()).unwrap(); + assert_eq!(result, "Hello World!\n"); + } + + #[test] + fn test_input_output() { + let program = String::from(",[.,]"); + let mut bf = BrainfuckProgram::new(program); + + let input_data = b"Brainfuck\n".to_vec(); + let input = Cursor::new(input_data); + let mut output = Cursor::new(vec![]); + + bf.run(input, &mut output).unwrap(); + + let result = String::from_utf8(output.into_inner()).unwrap(); + println!("{}", result.clone()); + assert_eq!(result, "Brainfuck\n"); + } + + #[tokio::test] + async fn test_input_output_async() { + let program = String::from(",[.,]"); + let mut bf = BrainfuckProgram::new(program); + + let input_data = b"Brainfuck\n".to_vec(); + let input = Cursor::new(input_data); + let mut output = Cursor::new(vec![]); + + let res = bf.run_async(input, &mut output).await; + match res { + Ok(n) => println!("Wooooo! {}", n), + Err(_) => { + println!("Boooo!"); + }, + }; + + let result = String::from_utf8(output.into_inner()).unwrap(); + println!("{}", result.clone()); + assert_eq!(result, "Brainfuck\n"); + } + + #[test] + fn test_hello_world() { + let program = r#" + ++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>. + "#; + let stdio = std::io::stdin(); + let stdin = stdio.lock(); + let stdout = std::io::stdout(); + let mut bf = BrainfuckProgram::new(program.to_string()); + if let Err(_) = bf.run(stdin, stdout) { + assert!(false) + } + } + + #[tokio::test] + async fn test_calculator() { + let program = r#" + +>+>+>+>>>,.>++++[<---------->-]<-------[-<[>>+<<-]>>[<<++++++++++>>-]<[<+>-],.>++++[<---------->-]<--[>+<-]>[<<<<<<<->>>>>>>-[<<<<<<->>>>>>--[<<<<<->>>>>--[<<<<<<<+>+>+>>>>>[<+>-]]]]]<]>,.>++++[<---------->-]<-------[-<[>>+<<-]>>[<<++++++++++>>-]<[<+>-],.>++++[<---------->-]<-------[>+>+<<-]>>[<<+>>-]<-[-[-[-[-[-[-[-[-[-[<[-]>[-]]]]]]]]]]]<]<<<<<<<[->->->->>[>>+<<-]>[>[<<+>>>+<-]>[<+>-]<<-]>[-]<<<<<<<]>[->->->>>[<+>-]<<<<<]>[->->+>>[>+<-]>>+<[>-<[<+>-]]>[-<<<<->[>+<-]>>>]<<<[->-[>+<-]>>+<[>-<[<+>-]]>[-<<<<->[>+<-]>>>]<<<]>[<+>-]<<<<]>[->>>>>+[-<<<[>>>+>+<<<<-]>>>[<<<+>>>-]<<[>>+>>+<<<<-]>>[<<+>>-]>[->->>+<<[>+<-]>[>-<[<+>-]]>[-<<<<+<+<<[-]>>>>[<<<<+>>>>-]>>>]<<<]>[-]<<]<<[-]<[>+<-]>>[<<+>>-]<<<<]>>>[>>+[<<[>>>+>+<<<<-]>>>>[<<<<+>>>>-]+<[-[-[-[-[-[-[-[-[-[>-<<<<---------->+>>[-]]]]]]]]]]]>[->[>]>++++[<++++++++++>-]<++++++++[<]<<<<[>>>>>[>]<+[<]<<<<-]>>-<[>+<[<+>-]]>>>]<<]>>>[>]>++++[<++++++++++>-]<++++++>>++++[<++++++++++>-]<++++++>>++++[<++++++++++>-]<++++++[<]<<<<]>+[<<[>>>+>+<<<<-]>>>>[<<<<+>>>>-]+<[-[-[-[-[-[-[-[-[-[>-<<<<---------->+>>[-]]]]]]]]]]]>[->>[>]>++++[<++++++++++>-]<++++++++[<]<<<<<[>>>>>>[>]<+[<]<<<<<-]>>-<[>+<[<+>-]]>>>]<<]<<<[->>>>>>>[>]>++++[<++++++++++>-]<+++++[<]<<<<<<]>>>>>>>[>]<[.<] + "#; + let input = BufReader::new(&b"2+100\n"[..]); + let stdout = std::io::stdout(); + let mut bf = BrainfuckProgram::new(program.to_string()); + let _ = bf.run(input, stdout); + } + + #[tokio::test] + async fn test_async() { + let program = r#" + >+++++++++++[-<+++++++++++++++>] # initialize 165 at first cell + >++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>>[-]>>>++++++++++<[->-[>+>>]>[+[- + <+>]>+>>]<<<<<]>[-]>>[>++++++[-<++++++++>]<.<<+>+>[-]]<[<[->-<]++++++[->++++++++ + <]>.[-]]<<++++++[-<++++++++>]<.[-]<<[-<+>] + "#; + let input = Cursor::new(&b"100\n"[..]); + let output = Cursor::new(vec![]); + + let mut bf = BrainfuckProgram::new(program.to_string()); + if let Err(_) = bf.run_async(input, output).await { + assert!(false) + } + } +} diff --git a/crack-core/Cargo.toml b/crack-core/Cargo.toml index 337bc2d66..fd7627dfa 100644 --- a/crack-core/Cargo.toml +++ b/crack-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "crack-core" -version = "0.3.7" +version = "0.3.8" authors = ["Cycle Five "] edition = "2021" description = "Core module for the cracking smoking, discord-music-bot Cracktunes." @@ -9,8 +9,10 @@ license = "MIT" keywords = ["music", "discord", "bot", "crack", "tunes", "cracktunes"] categories = ["multimedia::audio"] homepage = "https://cracktun.es/" -repository = "https://git.sr.ht/~cycle-five/cracktunes" - +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +#repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +workspace = "../" [features] default = ["cache", "playlist", "ignore-presence-log"] @@ -21,9 +23,10 @@ cache = ["serenity/cache", "poise/cache"] crack-metrics = ["prometheus"] crack-gpt = ["dep:crack-gpt"] crack-osint = ["dep:crack-osint"] +crack-bf = ["dep:crack-bf"] [dependencies] -rusty_ytdl = { git = "https://github.com/cycle-five/rusty_ytdl", default-features = false, branch = "v0.7.2-boa", features = [ +rusty_ytdl = { git = "https://github.com/cycle-five/rusty_ytdl", default-features = false, branch = "main", features = [ "live", "rustls-tls", "search", @@ -31,66 +34,64 @@ rusty_ytdl = { git = "https://github.com/cycle-five/rusty_ytdl", default-feature "ffmpeg", ] } audiopus = "0.3.0-rc.0" -async-trait = "0.1.80" -anyhow = "1.0.83" -bytes = "1.6.0" -colored = "2.1.0" -lazy_static = "1.4.0" -lyric_finder = { git = "https://github.com/cycle-five/spotify-player", branch = "master", version = "0.1.6" } -rand = "0.8.5" -regex = "1.10.4" -serde = { version = "1.0.202", features = ["derive", "rc"] } -serde_json = "1.0.117" -serde_with = "3.8.1" -url = "2.5.0" -sys-info = "0.9.1" -prometheus = { version = "0.13.4", features = ["process"], optional = true } -proc-macro2 = "1.0.82" -typemap_rev = "0.3.0" -either = "1.11.0" -chrono = { version = "0.4.38", features = ["serde"] } -once_cell = "1.19.0" -reqwest = { version = "0.12.4", default-features = false, features = [ - "blocking", - "json", - "multipart", - "rustls-tls", - "cookies", -] } +async-trait = "0.1" +anyhow = "1.0" +bytes = "1.6" +colored = "2.1" +lazy_static = "1.5" +lyric_finder = { git = "https://github.com/cycle-five/spotify-player", branch = "master", features = ["rustls-tls"], version = "0.1.7" } +rand = "0.8" +regex = "1.10" +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = "1.0" +# serde_with = "3.8" +url = "2.5" +sys-info = "0.9" +prometheus = { version = "0.13", features = ["process"], optional = true } +typemap_rev = "0.3" +either = "1.12" +chrono = { version = "0.4", features = ["serde"] } +once_cell = "1.19" +strsim = "0.11" +itertools = "0.13" +dashmap = "6.0" +indexmap = "2.2" crack-gpt = { path = "../crack-gpt", optional = true } crack-osint = { path = "../crack-osint", optional = true } +crack-bf = { path = "../crack-bf", optional = true } + +reqwest = { workspace = true } tracing = { workspace = true } sqlx = { workspace = true } serenity = { workspace = true } songbird = { workspace = true } tokio = { workspace = true } poise = { workspace = true } - -[dependencies.symphonia] -version = "0.5.4" -# features = ["all-formats", "all-codecs", "opt-simd"] -features = ["aac", "mp3", "isomp4", "alac"] +symphonia = { workspace = true } [dependencies.serenity-voice-model] -version = "0.2.0" +version = "0.2" [dependencies.rspotify] -version = "0.13.1" +version = "0.13" default-features = false features = ["client-reqwest", "reqwest-rustls-tls"] -[dependencies.ffprobe] -git = "https://github.com/cycle-five/ffprobe-rs" -features = ["async-tokio"] -version = "0.4.0" +# [dependencies.ffprobe] +# git = "https://github.com/cycle-five/ffprobe-rs" +# features = ["async-tokio"] +# version = "0.4.0" + +# [workspace.metadata.dist] +# dist = false [dev-dependencies] -ctor = "0.2.8" -mockall = "0.12.1" -tungstenite = "0.21.0" -async-tungstenite = "0.25.1" -sqlx = { version = "0.7.4", features = [ +ctor = "0.2" +mockall = "0.12" +tungstenite = "0.23" +async-tungstenite = "0.26" +sqlx = { version = "0.7", features = [ "runtime-tokio", "tls-rustls", "macros", @@ -100,3 +101,6 @@ sqlx = { version = "0.7.4", features = [ "migrate", "json", ] } + +[build-dependencies] +vergen = { version = "8", features = ["git", "cargo", "si", "build", "gitcl"] } diff --git a/crack-core/build.rs b/crack-core/build.rs index b43a49d88..942775a51 100644 --- a/crack-core/build.rs +++ b/crack-core/build.rs @@ -1,12 +1,9 @@ -use std::process::Command; -fn main() { +fn main() -> Result<(), Box> { // make sure tarpaulin is included in the build - println!("cargo::rustc-check-cfg=cfg(tarpaulin_include)"); - // note: add error checking yourself. - let output = Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .unwrap(); - let git_hash = String::from_utf8(output.stdout).unwrap(); - println!("cargo:rustc-env=GIT_HASH={}", git_hash); + println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)"); + vergen::EmitBuilder::builder() + .all_build() + .all_git() + .emit()?; + Ok(()) } diff --git a/crack-core/src/commands/admin/authorize.rs b/crack-core/src/commands/admin/authorize.rs index 3616b3512..6f36ce026 100644 --- a/crack-core/src/commands/admin/authorize.rs +++ b/crack-core/src/commands/admin/authorize.rs @@ -1,14 +1,20 @@ use crate::errors::CrackedError; use crate::guild::settings::GuildSettings; -use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::messaging::{message::CrackedMessage, messages::UNKNOWN}; +use crate::utils::send_reply; use crate::Context; use crate::Error; use poise::serenity_prelude::Mentionable; use serenity::all::User; /// Utilizes the permissions v2 `required_permissions` field -#[poise::command(slash_command, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Admin", + prefix_command, + slash_command, + required_permissions = "ADMINISTRATOR" +)] +#[cfg(not(tarpaulin_include))] pub async fn check_admin(ctx: Context<'_>) -> Result<(), Error> { ctx.say("Authorized.").await?; @@ -23,6 +29,7 @@ pub async fn authorize( #[description = "The user to add to authorized list"] user: User, ) -> Result<(), Error> { // let id = user_id.parse::().expect("Failed to parse user id"); + let mention = user.mention(); let id = user.id; let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; @@ -55,9 +62,9 @@ pub async fn authorize( .to_partial_guild(ctx.http()) .await .map(|g| g.name) - .unwrap_or_else(|_| "Unknown".to_string()); - let msg = send_response_poise( - ctx, + .unwrap_or_else(|_| UNKNOWN.to_string()); + let _ = send_reply( + &ctx, CrackedMessage::UserAuthorized { id, mention, @@ -67,6 +74,5 @@ pub async fn authorize( true, ) .await?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/admin/ban.rs b/crack-core/src/commands/admin/ban.rs index 4d9d510b3..1034cc517 100644 --- a/crack-core/src/commands/admin/ban.rs +++ b/crack-core/src/commands/admin/ban.rs @@ -1,6 +1,6 @@ use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use poise::serenity_prelude::Mentionable; @@ -9,7 +9,7 @@ use serenity::all::User; /// Ban a user from the server. // There really doesn't seem to be a good way to restructure commands like this // in a way that allows for unit testing. -// 1) Almost every call relies on the ctx, cache, or http, and these are basically +// 1) Almost every call relies on the &ctx, cache, or http, and these are basically // impossible to mock. // 2) Even trying to segragate the logic in the reponse creation pieces is difficult // due to the fact that we're using poise to do prefix and slash commands at the @@ -17,6 +17,7 @@ use serenity::all::User; // of command and thus the context. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Admin", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", @@ -27,7 +28,7 @@ pub async fn ban( #[description = "User to ban."] user: User, #[description = "Number of day to delete messages of the user."] dmd: Option, #[rest] - #[description = "Reason to the ban."] + #[description = "Reason for the ban."] reason: Option, ) -> Result<(), Error> { let mention = user.mention(); @@ -38,15 +39,15 @@ pub async fn ban( let guild = guild_id.to_partial_guild(&ctx).await?; if let Err(e) = guild.ban_with_reason(&ctx, user.id, dmd, reason).await { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to ban user: {}", e)), true, ) .await?; } else { // Send success message - send_response_poise(ctx, CrackedMessage::UserBanned { mention, id }, true).await?; + send_reply(&ctx, CrackedMessage::UserBanned { mention, id }, true).await?; } Ok(()) } diff --git a/crack-core/src/commands/admin/broadcast_voice.rs b/crack-core/src/commands/admin/broadcast_voice.rs index ba6139c0e..5a2f67181 100644 --- a/crack-core/src/commands/admin/broadcast_voice.rs +++ b/crack-core/src/commands/admin/broadcast_voice.rs @@ -1,4 +1,4 @@ -use crate::{Context, ContextExt, Error}; +use crate::{poise_ext::ContextExt, Context, Error}; use serenity::all::CreateMessage; /// Broadcast a message to all guilds where the bot is currently in a voice channel. diff --git a/crack-core/src/commands/admin/create_text_channel.rs b/crack-core/src/commands/admin/create_text_channel.rs index 1c54e78a0..e9a9eef87 100644 --- a/crack-core/src/commands/admin/create_text_channel.rs +++ b/crack-core/src/commands/admin/create_text_channel.rs @@ -2,7 +2,7 @@ use serenity::builder::CreateChannel; use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; @@ -32,8 +32,8 @@ pub async fn create_text_channel( { Err(e) => { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to create channel: {}", e)), true, ) @@ -41,8 +41,8 @@ pub async fn create_text_channel( }, Ok(channel) => { // Send success message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::TextChannelCreated { channel_name: channel.name.clone(), channel_id: channel.id, @@ -80,8 +80,8 @@ pub async fn create_category( { Err(e) => { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to create channel: {}", e)), true, ) @@ -89,8 +89,8 @@ pub async fn create_category( }, Ok(channel) => { // Send success message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::TextChannelCreated { channel_name: channel.name.clone(), channel_id: channel.id, diff --git a/crack-core/src/commands/admin/create_voice_channel.rs b/crack-core/src/commands/admin/create_voice_channel.rs index a007ac5ee..34ec216a0 100644 --- a/crack-core/src/commands/admin/create_voice_channel.rs +++ b/crack-core/src/commands/admin/create_voice_channel.rs @@ -2,7 +2,7 @@ use serenity::builder::CreateChannel; use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; @@ -31,16 +31,16 @@ pub async fn create_voice_channel( .await { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to create channel: {}", e)), true, ) .await?; } else { // Send success message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::VoiceChannelCreated { channel_name: channel_name.clone(), }, diff --git a/crack-core/src/commands/admin/deafen.rs b/crack-core/src/commands/admin/deafen.rs index 2fcb2d072..5e9873fc5 100644 --- a/crack-core/src/commands/admin/deafen.rs +++ b/crack-core/src/commands/admin/deafen.rs @@ -1,11 +1,8 @@ -use std::sync::Arc; - use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; -use serenity::all::Context as SerenityContext; use serenity::all::GuildId; use serenity::all::Mentionable; use serenity::builder::EditMember; @@ -24,16 +21,9 @@ pub async fn deafen( #[description = "User to deafen"] user: serenity::model::user::User, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let crack_msg = deafen_internal( - Arc::new(ctx.serenity_context().clone()), - guild_id, - user.clone(), - true, - ) - .await?; + let crack_msg = deafen_internal(&ctx, guild_id, user.clone(), true).await?; // Handle error, send error message - let sent_msg = send_response_poise(ctx, crack_msg, true).await?; - ctx.data().add_msg_to_cache(guild_id, sent_msg); + let _ = send_reply(&ctx, crack_msg, true).await?; Ok(()) } @@ -51,22 +41,16 @@ pub async fn undeafen( #[description = "User to undeafen"] user: serenity::model::user::User, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let crack_msg = deafen_internal( - Arc::new(ctx.serenity_context().clone()), - guild_id, - user.clone(), - false, - ) - .await?; + let crack_msg = deafen_internal(&ctx, guild_id, user.clone(), false).await?; // Handle error, send error message - let sent_msg = send_response_poise(ctx, crack_msg, true).await?; - ctx.data().add_msg_to_cache(guild_id, sent_msg); + let _ = send_reply(&ctx, crack_msg, true).await?; Ok(()) } /// Deafen or undeafen a user. pub async fn deafen_internal( - ctx: Arc, + //ctx: Arc, + cache_http: &impl serenity::prelude::CacheHttp, guild_id: GuildId, user: serenity::model::user::User, deafen: bool, @@ -74,7 +58,11 @@ pub async fn deafen_internal( let mention = user.clone().mention(); let id = user.clone().id; let msg = if let Err(e) = guild_id - .edit_member(&ctx, user.clone().id, EditMember::new().deafen(deafen)) + .edit_member( + cache_http, + user.clone().id, + EditMember::new().deafen(deafen), + ) .await { let msg = if deafen { diff --git a/crack-core/src/commands/admin/deauthorize.rs b/crack-core/src/commands/admin/deauthorize.rs index 616d2f3a7..0df43c24e 100644 --- a/crack-core/src/commands/admin/deauthorize.rs +++ b/crack-core/src/commands/admin/deauthorize.rs @@ -1,5 +1,6 @@ use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::messaging::messages::UNKNOWN; +use crate::utils::send_reply; use crate::Context; use crate::Error; use poise::serenity_prelude::Mentionable; @@ -10,7 +11,8 @@ use poise::serenity_prelude::Mentionable; slash_command, prefix_command, required_permissions = "ADMINISTRATOR", - owners_only + owners_only, + category = "admin" )] pub async fn deauthorize( ctx: Context<'_>, @@ -26,7 +28,7 @@ pub async fn deauthorize( .to_partial_guild(ctx) .await .map(|g| g.name) - .unwrap_or_else(|_| "Unknown".to_string()); + .unwrap_or_else(|_| UNKNOWN.to_string()); let res = ctx .data() @@ -49,8 +51,8 @@ pub async fn deauthorize( tracing::info!("User Deauthorized: UserId = {}, GuildId = {}", id, res); let mention = user.mention(); - let msg = send_response_poise( - ctx, + let _ = send_reply( + &ctx, CrackedMessage::UserDeauthorized { id, mention, @@ -61,7 +63,5 @@ pub async fn deauthorize( ) .await?; - ctx.data().add_msg_to_cache(guild_id, msg); - Ok(()) } diff --git a/crack-core/src/commands/admin/debug.rs b/crack-core/src/commands/admin/debug.rs index 6e434b3d2..c2151c160 100644 --- a/crack-core/src/commands/admin/debug.rs +++ b/crack-core/src/commands/admin/debug.rs @@ -8,7 +8,7 @@ use songbird::tracks::TrackQueue; /// Print some debug info. #[poise::command(prefix_command, owners_only, ephemeral)] -pub async fn debug(ctx: Context<'_>) -> Result<(), Error> { +pub async fn debugold(ctx: Context<'_>) -> Result<(), Error> { let data = ctx.data(); let data_str = format!("{:#?}", data); @@ -33,7 +33,7 @@ pub async fn debug(ctx: Context<'_>) -> Result<(), Error> { None => { let embed = CreateEmbed::default().description(format!("{}", CrackedError::NotConnected)); - send_embed_response_poise(ctx, embed).await?; + send_embed_response_poise(&ctx, embed).await?; return Ok(()); }, }; @@ -47,7 +47,7 @@ pub async fn debug(ctx: Context<'_>) -> Result<(), Error> { "data: {}old_data_str{}\nqueue: {}", data_str, old_data_str, queue_str )); - send_embed_response_poise(ctx, embed).await?; + send_embed_response_poise(&ctx, embed).await?; Ok(()) } diff --git a/crack-core/src/commands/admin/delete_channel.rs b/crack-core/src/commands/admin/delete_channel.rs index 5a2a06dc9..f288250a4 100644 --- a/crack-core/src/commands/admin/delete_channel.rs +++ b/crack-core/src/commands/admin/delete_channel.rs @@ -1,6 +1,6 @@ use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; @@ -28,16 +28,16 @@ pub async fn delete_channel( if let Some((channel_id, guild_chan)) = channel { if let Err(e) = guild_chan.delete(&ctx).await { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to delete channel: {}", e)), true, ) .await?; } else { // Send success message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::ChannelDeleted { channel_id, channel_name: channel_name.clone(), @@ -47,8 +47,8 @@ pub async fn delete_channel( .await?; } } else { - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other("Channel not found.".to_string()), true, ) diff --git a/crack-core/src/commands/admin/kick.rs b/crack-core/src/commands/admin/kick.rs index c435f5c3d..836e4969b 100644 --- a/crack-core/src/commands/admin/kick.rs +++ b/crack-core/src/commands/admin/kick.rs @@ -1,7 +1,7 @@ use crate::errors::CrackedError; use crate::guild::operations::GuildSettingsOperations; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use serenity::all::{Mentionable, User}; @@ -27,15 +27,15 @@ pub async fn kick( let as_embed = ctx.data().get_reply_with_embed(guild_id).await; let guild = guild_id.to_partial_guild(&ctx).await?; if let Err(e) = guild.kick(&ctx, id).await { - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to kick user: {}", e)), as_embed, ) .await?; } else { // Send success message - send_response_poise(ctx, CrackedMessage::UserKicked { id, mention }, as_embed).await?; + send_reply(&ctx, CrackedMessage::UserKicked { id, mention }, as_embed).await?; } Ok(()) } @@ -97,7 +97,7 @@ pub async fn rename_all( } let r = rand::random::() % names.len(); let random_name = names.remove(r).clone(); - let (emoji, new_name) = if let Some(cur_nick) = member.user.nick_in(ctx, guild_id).await { + let (emoji, new_name) = if let Some(cur_nick) = member.user.nick_in(&ctx, guild_id).await { // if cur_nick.contains("&") { // random_name = cur_nick.replace("&", "&"); // } @@ -162,14 +162,14 @@ pub async fn rename_all( // let guild = guild_id.to_partial_guild(&ctx).await?; // if let Err(e) = guild.kick(&ctx, user_id).await { // // Handle error, send error message -// send_response_poise( -// ctx, +// send_reply( +// &ctx, // CrackedMessage::Other(format!("Failed to kick user: {}", e)), // ) // .await?; // } else { // // Send success message -// send_response_poise(ctx, CrackedMessage::UserKicked { user_id }).await?; +// send_reply(&ctx, CrackedMessage::UserKicked { user_id }).await?; // } // Ok(()) // } diff --git a/crack-core/src/commands/admin/message_cache.rs b/crack-core/src/commands/admin/message_cache.rs index 3126d3034..5bbf3cfd5 100644 --- a/crack-core/src/commands/admin/message_cache.rs +++ b/crack-core/src/commands/admin/message_cache.rs @@ -1,15 +1,16 @@ use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise_text; +use crate::utils::send_reply; use crate::Context; +use crate::CrackedError; use crate::Error; /// Get the message cache. #[cfg(not(tarpaulin_include))] #[poise::command(prefix_command, owners_only, ephemeral)] pub async fn message_cache(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let cache_str = { - let mut message_cache = ctx.data().guild_msg_cache_ordered.lock().unwrap().clone(); + let mut message_cache = ctx.data().guild_msg_cache_ordered.lock().await.clone(); message_cache .entry(guild_id) .or_default() @@ -20,9 +21,7 @@ pub async fn message_cache(ctx: Context<'_>) -> Result<(), Error> { tracing::warn!("message_cache: {}", cache_str); - let msg = send_response_poise_text(ctx, CrackedMessage::Other(cache_str)).await?; - - ctx.data().add_msg_to_cache(guild_id, msg); + send_reply(&ctx, CrackedMessage::Other(cache_str), false).await?; Ok(()) } diff --git a/crack-core/src/commands/admin/mod.rs b/crack-core/src/commands/admin/mod.rs index 5ce84429a..0c1c6ce7c 100644 --- a/crack-core/src/commands/admin/mod.rs +++ b/crack-core/src/commands/admin/mod.rs @@ -19,8 +19,8 @@ pub mod random_mute_lol; pub mod role; pub mod set_vc_size; pub mod timeout; -pub mod unban; pub mod unmute; +pub mod user; use crate::{Context, Error}; pub use audit_logs::*; @@ -44,14 +44,19 @@ pub use random_mute_lol::*; pub use role::*; pub use set_vc_size::*; pub use timeout::*; -pub use unban::*; pub use unmute::*; +pub use user::*; +use crate::commands::help::sub_help as help; +use crate::messaging::message::CrackedMessage; +use crate::poise_ext::PoiseContextExt; /// Admin commands. #[poise::command( + category = "Admin", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", subcommands( "audit_logs", "authorize", @@ -68,14 +73,15 @@ pub use unmute::*; "mute", "message_cache", "move_users_to", - "unban", "undeafen", "unmute", "random_mute", "get_active_vcs", "set_vc_size", - "role", "timeout", + "user", + "role", + "help" ), ephemeral, // owners_only @@ -84,7 +90,26 @@ pub use unmute::*; pub async fn admin(ctx: Context<'_>) -> Result<(), Error> { tracing::warn!("Admin command called"); - ctx.say("You found the admin command").await?; + let msg = CrackedMessage::CommandFound("admin".to_string()); + ctx.send_reply(msg, true).await?; Ok(()) } + +/// List of all the admin commands. +pub fn admin_commands() -> Vec { + vec![ + admin(), + ban(), + kick(), + mute(), + unmute(), + deafen(), + undeafen(), + timeout(), + ] + .into_iter() + .chain(role::role_commands()) + .chain(user::user_commands()) + .collect() +} diff --git a/crack-core/src/commands/admin/move_users.rs b/crack-core/src/commands/admin/move_users.rs index 4786ec519..63b9e132e 100644 --- a/crack-core/src/commands/admin/move_users.rs +++ b/crack-core/src/commands/admin/move_users.rs @@ -34,7 +34,7 @@ pub async fn move_users_to( let mut member = ctx.http().get_member(guild_id, *user_id).await?; let _ = member - .edit(ctx, EditMember::new().voice_channel(guild_chan_to.id)) + .edit(&ctx, EditMember::new().voice_channel(guild_chan_to.id)) .await; } diff --git a/crack-core/src/commands/admin/mute.rs b/crack-core/src/commands/admin/mute.rs index 57878cfb1..b1aaa51ef 100644 --- a/crack-core/src/commands/admin/mute.rs +++ b/crack-core/src/commands/admin/mute.rs @@ -1,12 +1,10 @@ use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; -use crate::Context; -use crate::Error; -use poise::serenity_prelude::Mentionable; -use serenity::all::EditMember; -use serenity::all::{Context as SerenityContext, GuildId}; -use std::sync::Arc; +use crate::utils::send_reply; +use crate::{Context, Error}; + +use poise::serenity_prelude as serenity; +use serenity::{CacheHttp, EditMember, GuildId, Mentionable}; /// Mute a user. #[poise::command( @@ -20,14 +18,8 @@ pub async fn mute( #[description = "User to mute"] user: serenity::model::user::User, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let crack_msg = mute_internal( - Arc::new(ctx.serenity_context().clone()), - user, - guild_id, - true, - ) - .await?; - send_response_poise(ctx, crack_msg, true) + let crack_msg = mute_internal(&ctx, user, guild_id, true).await?; + send_reply(&ctx, crack_msg, true) .await .map(|_| ()) .map_err(Into::into) @@ -35,23 +27,17 @@ pub async fn mute( /// Unmute a user. pub async fn mute_internal( - ctx: Arc, - user: serenity::model::user::User, + cache_http: &impl CacheHttp, + user: serenity::User, guild_id: GuildId, mute: bool, ) -> Result { let mention = user.mention(); let id = user.id; if let Err(e) = guild_id - .edit_member(&ctx, user.clone().id, EditMember::new().mute(mute)) + .edit_member(cache_http, user.clone().id, EditMember::new().mute(mute)) .await { - // Handle error, send error message - // send_response_poise( - // ctx, - // CrackedMessage::Other(format!("Failed to mute user: {}", e)), - // ) - // .await Ok(CrackedMessage::Other(format!("Failed to mute user: {}", e))) } else { // Send success message diff --git a/crack-core/src/commands/admin/random_mute_lol.rs b/crack-core/src/commands/admin/random_mute_lol.rs index be22d227f..2598865ab 100644 --- a/crack-core/src/commands/admin/random_mute_lol.rs +++ b/crack-core/src/commands/admin/random_mute_lol.rs @@ -62,12 +62,12 @@ impl EventHandler for RandomMuteHandler { // let member = guild.member(&self.ctx, self.user.id).await.unwrap(); let r = rand::thread_rng().gen_range(0..100); if r < 50 { - let _msg = mute_internal(self.ctx.clone(), self.user.clone(), self.guild_id, true) + let _msg = mute_internal(&self.ctx, self.user.clone(), self.guild_id, true) .await .unwrap(); // } else if r < 75 { } else { - let _msg = mute_internal(self.ctx.clone(), self.user.clone(), self.guild_id, false) + let _msg = mute_internal(&self.ctx, self.user.clone(), self.guild_id, false) .await .unwrap(); } diff --git a/crack-core/src/commands/admin/role/assign_role.rs b/crack-core/src/commands/admin/role/assign_role.rs index 61c4ce2a2..3f108d6f0 100644 --- a/crack-core/src/commands/admin/role/assign_role.rs +++ b/crack-core/src/commands/admin/role/assign_role.rs @@ -1,18 +1,24 @@ -use crate::errors::CrackedError; -use crate::Context; -use crate::Error; +use crate::commands::sub_help as help; +use crate::{Context, Error}; use serenity::all::{GuildId, Member, Role, RoleId, UserId}; /// Assign role. -#[poise::command(prefix_command, owners_only, ephemeral)] +#[poise::command( + category = "Admin", + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", + prefix_command, + slash_command, + subcommands("help"), + hide_in_help = true, + ephemeral +)] #[cfg(not(tarpaulin_include))] pub async fn assign( ctx: Context<'_>, #[description = "Role to assign"] role: Role, #[description = "Member to assign the role to"] member: Member, ) -> Result<(), Error> { - let _guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - member .add_role(&ctx, role) .await @@ -21,7 +27,16 @@ pub async fn assign( } /// Assign role. -#[poise::command(prefix_command, owners_only, ephemeral)] +#[poise::command( + category = "Admin", + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", + prefix_command, + slash_command, + subcommands("help"), + hide_in_help = true, + ephemeral +)] #[cfg(not(tarpaulin_include))] pub async fn assign_ids( ctx: Context<'_>, @@ -29,8 +44,6 @@ pub async fn assign_ids( #[description = "RoleId to assign"] role_id: RoleId, #[description = "UserId to assign role to"] user_id: UserId, ) -> Result<(), Error> { - // let _guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let member = guild_id.member(&ctx, user_id).await?; member .add_role(&ctx, role_id) diff --git a/crack-core/src/commands/admin/role/create_role.rs b/crack-core/src/commands/admin/role/create_role.rs index 186345273..36940bdc8 100644 --- a/crack-core/src/commands/admin/role/create_role.rs +++ b/crack-core/src/commands/admin/role/create_role.rs @@ -2,15 +2,24 @@ use poise::serenity_prelude::{Colour, Permissions}; use serenity::all::{Attachment, CreateAttachment, GuildId, Role}; use serenity::builder::EditRole; -use crate::commands::{ConvertToEmptyResult, EmptyResult}; +use crate::commands::{sub_help as help, EmptyResult}; use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; /// Create role. #[allow(clippy::too_many_arguments)] -#[poise::command(prefix_command, owners_only, ephemeral)] +#[poise::command( + category = "Admin", + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", + prefix_command, + slash_command, + subcommands("help"), + hide_in_help = true, + ephemeral +)] pub async fn create( ctx: Context<'_>, #[description = "Name of the role to create."] name: String, @@ -21,13 +30,13 @@ pub async fn create( #[description = "Optional initial colour"] colour: Option, #[description = "Optional emoji"] unicode_emoji: Option, #[description = "Optional reason for the audit_log"] audit_log_reason: Option, - #[description = "Optional initial perms"] icon: Option, + #[description = "Optional icon"] icon: Option, ) -> EmptyResult { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; let icon = match icon { Some(attachment) => { let url = attachment.url.clone(); - Some(CreateAttachment::url(ctx, &url).await?) + Some(CreateAttachment::url(&ctx, &url).await?) }, None => None, }; @@ -47,8 +56,8 @@ pub async fn create( ) .await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::RoleCreated { role_name: role.name.clone(), role_id: role.id, @@ -56,7 +65,8 @@ pub async fn create( true, ) .await - .convert() + .map(|_| ()) + .map_err(Into::into) } /// Internal create role function. diff --git a/crack-core/src/commands/admin/role/delete_role.rs b/crack-core/src/commands/admin/role/delete_role.rs index 103ea04c3..52edb5174 100644 --- a/crack-core/src/commands/admin/role/delete_role.rs +++ b/crack-core/src/commands/admin/role/delete_role.rs @@ -1,13 +1,20 @@ -use serenity::all::{Message, Role, RoleId}; +use poise::ReplyHandle; +use serenity::all::{Role, RoleId}; use crate::{ - errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, - Error, + errors::CrackedError, messaging::message::CrackedMessage, utils::send_reply, Context, Error, }; /// Delete role. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, owners_only, ephemeral)] +#[poise::command( + category = "Admin", + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", + prefix_command, + hide_in_help = true, + ephemeral +)] pub async fn delete( ctx: Context<'_>, #[description = "Role to delete."] mut role: Role, @@ -17,7 +24,14 @@ pub async fn delete( /// Delete role by id #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, owners_only, ephemeral)] +#[poise::command( + category = "Admin", + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "ADMINISTRATOR", + prefix_command, + hide_in_help = true, + ephemeral +)] pub async fn delete_by_id( ctx: Context<'_>, #[description = "RoleId to delete."] role_id: RoleId, @@ -28,27 +42,26 @@ pub async fn delete_by_id( .map(|_| ()) } +use std::borrow::Cow; /// Delete role helper. pub async fn delete_role_by_id_helper( ctx: Context<'_>, role_id: u64, -) -> Result { +) -> Result, CrackedError> { let role_id = RoleId::new(role_id); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let mut role = guild_id - .roles(&ctx) + .roles(&ctx.clone()) .await? .into_iter() .find(|r| r.0 == role_id) .ok_or(CrackedError::RoleNotFound(role_id))?; - role.1.delete(&ctx).await?; + role.1.delete(&ctx.clone()).await?; // Send success message - send_response_poise( - ctx, - CrackedMessage::RoleDeleted { - role_name: role.1.name.clone(), - role_id, - }, + let role_name: Cow<'_, String> = Cow::Owned(role.1.name.to_string()); + send_reply( + &ctx, + CrackedMessage::RoleDeleted { role_id, role_name }, true, ) .await diff --git a/crack-core/src/commands/admin/role/mod.rs b/crack-core/src/commands/admin/role/mod.rs index 54b7d8cde..60fa1b7f3 100644 --- a/crack-core/src/commands/admin/role/mod.rs +++ b/crack-core/src/commands/admin/role/mod.rs @@ -6,26 +6,29 @@ pub use assign_role::*; pub use create_role::*; pub use delete_role::*; +pub use crate::poise_ext::ContextExt; +pub use crate::utils; + +use crate::commands::sub_help as help; use crate::{Context, Error}; + /// Role commands. #[poise::command( prefix_command, - //slash_command, - subcommands( - "create", - "delete", - "delete_by_id", - "assign", - "assign_ids", - ), + slash_command, + subcommands("create", "delete", "delete_by_id", "assign", "assign_ids", "help"), ephemeral, - owners_only + hide_in_help = true )] #[cfg(not(tarpaulin_include))] pub async fn role(ctx: Context<'_>) -> Result<(), Error> { tracing::warn!("Role command called"); - ctx.say("You found the role command").await?; + ctx.send_found_command("admin role".to_string()).await?; Ok(()) } + +pub fn role_commands() -> [crate::Command; 5] { + [assign(), assign_ids(), create(), delete(), delete_by_id()] +} diff --git a/crack-core/src/commands/admin/set_vc_size.rs b/crack-core/src/commands/admin/set_vc_size.rs index ec5efec8e..2e1eab36c 100644 --- a/crack-core/src/commands/admin/set_vc_size.rs +++ b/crack-core/src/commands/admin/set_vc_size.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; use crate::messaging::messages::UNKNOWN_LIT; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use serenity::all::Channel; @@ -27,8 +27,8 @@ pub async fn set_vc_size( .id() .edit(&ctx, EditChannel::new().user_limit(size)) .await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Channel size sent to {size}")), true, ) diff --git a/crack-core/src/commands/admin/timeout.rs b/crack-core/src/commands/admin/timeout.rs index 69f04c455..fbf261e64 100644 --- a/crack-core/src/commands/admin/timeout.rs +++ b/crack-core/src/commands/admin/timeout.rs @@ -1,6 +1,6 @@ use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use poise::serenity_prelude::Mentionable; @@ -54,8 +54,8 @@ pub async fn timeout( { // Handle error, send error message tracing::error!("Failed to timeout user: {}", e); - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to timeout user: {}", e)), true, ) @@ -68,7 +68,7 @@ pub async fn timeout( timeout_until: timeout_until.clone(), }; tracing::info!("User timed out: {}", msg); - send_response_poise(ctx, msg, true).await?; + send_reply(&ctx, msg, true).await?; } Ok(()) } diff --git a/crack-core/src/commands/admin/unban.rs b/crack-core/src/commands/admin/unban.rs deleted file mode 100644 index 079308668..000000000 --- a/crack-core/src/commands/admin/unban.rs +++ /dev/null @@ -1,62 +0,0 @@ -use crate::errors::CrackedError; -use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; -use crate::Context; -use crate::Error; -use poise::serenity_prelude::Mentionable; -use serenity::all::GuildId; -use serenity::all::User; -use serenity::all::UserId; - -/// Unban command -#[poise::command( - slash_command, - prefix_command, - required_permissions = "ADMINISTRATOR", - ephemeral -)] -#[cfg(not(tarpaulin_include))] -pub async fn unban( - ctx: Context<'_>, - #[description = "User to unban"] user: serenity::model::user::User, -) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - unban_helper(ctx, guild_id, user).await -} - -/// Unban a user from the server. -#[poise::command(prefix_command, owners_only, ephemeral)] -#[cfg(not(tarpaulin_include))] -pub async fn unban_by_user_id( - ctx: Context<'_>, - #[description = "UserId to unban"] user_id: UserId, -) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - unban_helper(ctx, guild_id, user_id.to_user(ctx).await?).await -} - -/// Unban a user from the server. -#[cfg(not(tarpaulin_include))] -pub async fn unban_helper(ctx: Context<'_>, guild_id: GuildId, user: User) -> Result<(), Error> { - let guild = guild_id.to_partial_guild(&ctx).await?; - let id = user.id; - let mention = user.mention(); - if let Err(e) = guild.unban(&ctx, user.id).await { - // Handle error, send error message - send_response_poise( - ctx, - CrackedMessage::Other(format!("Failed to unban user: {}", e)), - true, - ) - .await - .map(|m| ctx.data().add_msg_to_cache(guild_id, m)) - .map(|_| ()) - } else { - // Send success message - send_response_poise(ctx, CrackedMessage::UserUnbanned { id, mention }, true) - .await - .map(|m| ctx.data().add_msg_to_cache(guild_id, m)) - .map(|_| ()) - } - .map_err(Into::into) -} diff --git a/crack-core/src/commands/admin/unmute.rs b/crack-core/src/commands/admin/unmute.rs index 3ef5c41eb..1a945c2ef 100644 --- a/crack-core/src/commands/admin/unmute.rs +++ b/crack-core/src/commands/admin/unmute.rs @@ -1,10 +1,9 @@ use crate::errors::CrackedError; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use poise::serenity_prelude::Mentionable; -use serenity::all::Message; use serenity::builder::EditMember; /// Unmute a user. @@ -27,29 +26,26 @@ pub async fn unmute( /// Unmute a user /// impl for other internal use. #[cfg(not(tarpaulin_include))] -pub async fn unmute_impl( - ctx: Context<'_>, - user: serenity::model::user::User, -) -> Result { +pub async fn unmute_impl(ctx: Context<'_>, user: serenity::model::user::User) -> Result<(), Error> { let id = user.id; let mention = user.mention(); let guild_id = ctx .guild_id() .ok_or(CrackedError::Other("Guild ID not found"))?; if let Err(e) = guild_id - .edit_member(ctx, user.clone().id, EditMember::new().mute(false)) + .edit_member(&ctx, user.clone().id, EditMember::new().mute(false)) .await { // Handle error, send error message - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Failed to unmute user: {}", e)), true, ) .await } else { // Send success message - send_response_poise(ctx, CrackedMessage::UserUnmuted { id, mention }, true).await - } - .map_err(Into::into) + send_reply(&ctx, CrackedMessage::UserUnmuted { id, mention }, true).await + }?; + Ok(()) } diff --git a/crack-core/src/commands/admin/user/mod.rs b/crack-core/src/commands/admin/user/mod.rs new file mode 100644 index 000000000..16e255c74 --- /dev/null +++ b/crack-core/src/commands/admin/user/mod.rs @@ -0,0 +1,32 @@ +pub mod unban; + +pub use unban::*; + +pub use crate::poise_ext::ContextExt; +pub use crate::utils; + +use crate::commands::sub_help as help; +use crate::{Command, Context, Error}; + +/// User admin commands. +#[poise::command( + category = "Admin", + prefix_command, + slash_command, + //subcommands("create", "delete", "delete_by_id", "assign", "assign_ids", "help"), + subcommands("help"), + ephemeral, + hide_in_help = true +)] +#[cfg(not(tarpaulin_include))] +pub async fn user(ctx: Context<'_>) -> Result<(), Error> { + tracing::warn!("Role command called"); + + ctx.send_found_command("admin user".to_string()).await?; + + Ok(()) +} + +pub fn user_commands() -> [Command; 2] { + [unban(), unban_by_user_id()] +} diff --git a/crack-core/src/commands/admin/user/unban.rs b/crack-core/src/commands/admin/user/unban.rs new file mode 100644 index 000000000..3a50e2948 --- /dev/null +++ b/crack-core/src/commands/admin/user/unban.rs @@ -0,0 +1,61 @@ +use crate::commands::sub_help as help; +use crate::errors::CrackedError; +use crate::messaging::message::CrackedMessage; +use crate::{poise_ext::PoiseContextExt, CommandResult, Context}; +use poise::serenity_prelude as serenity; +use serenity::{Mentionable, User, UserId}; + +/// Unban a user from the guild. +#[poise::command( + category = "Admin", + slash_command, + prefix_command, + subcommands("help"), + required_permissions = "ADMINISTRATOR", + ephemeral +)] +#[cfg(not(tarpaulin_include))] +pub async fn unban( + ctx: Context<'_>, + #[description = "User to unban."] user: User, +) -> CommandResult { + let user_id = user.id; + unban_internal(ctx, user_id).await +} + +/// Unban a user from the guild, by user id. +#[poise::command( + category = "Admin", + prefix_command, + slash_command, + subcommands("help"), + required_permissions = "ADMINISTRATOR", + ephemeral +)] +#[cfg(not(tarpaulin_include))] +pub async fn unban_by_user_id( + ctx: Context<'_>, + #[description = "UserId to unban"] user_id: UserId, +) -> CommandResult { + unban_internal(ctx, user_id).await +} + +/// Unban a user from the guild, by user id. +#[cfg(not(tarpaulin_include))] +pub async fn unban_internal(ctx: Context<'_>, user_id: UserId) -> CommandResult { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild = guild_id.to_partial_guild(&ctx).await?; + let user = user_id.to_user(&ctx).await?; + let mention = user.mention(); + let msg = if let Err(e) = guild.unban(&ctx, user_id).await { + CrackedMessage::Other(format!("Failed to unban user: {}", e)) + } else { + CrackedMessage::UserUnbanned { + id: user_id, + mention, + } + }; + + let _ = ctx.send_reply(msg, true).await?; + Ok(()) +} diff --git a/crack-core/src/commands/bf.rs b/crack-core/src/commands/bf.rs new file mode 100644 index 000000000..2b592e29d --- /dev/null +++ b/crack-core/src/commands/bf.rs @@ -0,0 +1,119 @@ +use crate::messaging::message::CrackedMessage; +use crate::{poise_ext::PoiseContextExt, Context, CrackedError, Error}; +use crack_bf::BrainfuckProgram; +use poise::ReplyHandle; +use std::io::Cursor; +use std::time::Duration; +use tokio::time::timeout; + +/// Brainfk interpreter. +#[poise::command(category = "Code", slash_command, prefix_command, user_cooldown = "30")] +pub async fn bf( + ctx: Context<'_>, + #[description = "Brainfk program to run."] program: String, + #[rest] + #[description = "Optional input to feed to the program on stdin."] + input: Option, +) -> Result<(), Error> { + bf_internal(ctx, program, input.unwrap_or_default()) + .await + .map(|_| ()) + .map_err(Into::into) +} + +// /// Select one of several stored brainfuck programs to load and run, then +// /// print the program source code. +// #[poise::command(slash_command, prefix_command)] +// pub async fn bf_select(ctx: Context<'_>) -> Result<(), Error> { +// let msg = send_brainfk_options(&ctx).await; +// // let selection = +// } + +// async fn send_brainfk_options(ctx: Context<'_>) -> Result {} + +/// Run a brainfk program. Program and input string maybe empty, +/// no handling is done for invalid programs. +pub async fn bf_internal( + ctx: Context<'_>, + program: String, + input: String, +) -> Result, CrackedError> { + tracing::info!("program: {program}, input: {input}"); + let mut bf = BrainfuckProgram::new(program); + + let arr_u8 = input.as_bytes(); + //let user_input = arr_u8; + let user_input = Cursor::new(arr_u8); + let mut output = Cursor::new(Vec::::with_capacity(32)); + + // let handle = HANDLE.lock().unwrap().clone().unwrap(); + //tokio::task::block_in_place(move || handle.block_on(async { bf.run(user_input, &mut output)).await } + + let n = timeout( + Duration::from_secs(30), + bf.run_async(user_input, &mut output), + ) + .into_inner() + .await?; + + let string_out = cursor_to_string(output, n)?; + tracing::info!("string_out\n{string_out}"); + let final_out = format!("```{string_out}```"); + ctx.send_reply(CrackedMessage::Other(final_out), false) + .await +} + +// async fn cursor_to_string(mut cur: Cursor>, n: usize) -> Result { +// //let mut output = Vec::with_capacity(n); +// let output = String::new(); +// let x = cur.into_inner().fill_buf().await?; +// tracing::info!("length: {}", x.len()); +// assert_eq!(n, x.len()); +// Ok(output) +// } + +fn cursor_to_string(cur: Cursor>, n: usize) -> Result { + //let mut output = Vec::with_capacity(n); + let x = cur.into_inner(); + tracing::info!("length: {}", x.len()); + assert_eq!(n, x.len()); + Ok(String::from_utf8_lossy(&x).to_string()) +} + +#[allow(dead_code)] +fn ascii_art_number() -> String { + let program = r#" + >>>>+>+++>+++>>>>>+++[ + >,+>++++[>++++<-]>[<<[-[->]]>[<]>-]<<[ + >+>+>>+>+[<<<<]<+>>[+<]<[>]>+[[>>>]>>+[<<<<]>-]+<+>>>-[ + <<+[>]>>+<<<+<+<--------[ + <<-<<+[>]>+<<-<<-[ + <<<+<-[>>]<-<-<<<-<----[ + <<<->>>>+<-[ + <<<+[>]>+<<+<-<-[ + <<+<-<+[>>]<+<<<<+<-[ + <<-[>]>>-<<<-<-<-[ + <<<+<-[>>]<+<<<+<+<-[ + <<<<+[>]<-<<-[ + <<+[>]>>-<<<<-<-[ + >>>>>+<-<<<+<-[ + >>+<<-[ + <<-<-[>]>+<<-<-<-[ + <<+<+[>]<+<+<-[ + >>-<-<-[ + <<-[>]<+<++++[<-------->-]++<[ + <<+[>]>>-<-<<<<-[ + <<-<<->>>>-[ + <<<<+[>]>+<<<<-[ + <<+<<-[>>]<+<<<<<-[ + >>>>-<<<-<- + ]]]]]]]]]]]]]]]]]]]]]]>[>[[[<<<<]>+>>[>>>>>]<-]<]>>>+>>>>>>>+>]< + ]<[-]<<<<<<<++<+++<+++[ + [>]>>>>>>++++++++[<<++++>++++++>-]<-<<[-[<+>>.<-]]<<<<[ + -[-[>+<-]>]>>>>>[.[>]]<<[<+>-]>>>[<<++[<+>--]>>-] + <<[->+<[<++>-]]<<<[<+>-]<<<< + ]>>+>>>--[<+>---]<.>>[[-]<<]< + ] + "#; + program.to_string() +} diff --git a/crack-core/src/commands/chatgpt.rs b/crack-core/src/commands/chatgpt.rs index 4407080dc..77e0ca716 100644 --- a/crack-core/src/commands/chatgpt.rs +++ b/crack-core/src/commands/chatgpt.rs @@ -4,13 +4,20 @@ use crack_gpt::GptContext; use poise::CreateReply; /// Chat with cracktunes using GPT-4o -#[poise::command(slash_command, prefix_command)] +#[poise::command( + category = "AI", + slash_command, + prefix_command, + user_cooldown = "30", + guild_cooldown = "30" +)] pub async fn chat( ctx: Context<'_>, #[rest] #[description = "Query to send to the model."] query: String, ) -> Result<(), Error> { + // Do we need this? ctx.defer().await?; let user_id = ctx.author().id.get(); diff --git a/crack-core/src/commands/help.rs b/crack-core/src/commands/help.rs new file mode 100644 index 000000000..3bb6bf30a --- /dev/null +++ b/crack-core/src/commands/help.rs @@ -0,0 +1,661 @@ +use crate::commands::CrackedError; +use crate::messaging::message::CrackedMessage; +use crate::messaging::messages::EXTRA_TEXT_AT_BOTTOM; +use crate::poise_ext::MessageInterfaceCtxExt; +use crate::utils::{create_paged_embed, send_reply}; +use crate::{require, Context, Data, Error}; +use itertools::Itertools; +use poise::builtins::HelpConfiguration; + +#[allow(clippy::unused_async)] +pub async fn autocomplete( + ctx: poise::ApplicationContext<'_, Data, Error>, + searching: &str, +) -> Vec { + fn flatten_commands( + result: &mut Vec, + commands: &[poise::Command], + searching: &str, + ) { + for command in commands { + if command.owners_only || command.hide_in_help { + continue; + } + + if command.subcommands.is_empty() { + if command.qualified_name.starts_with(searching) { + result.push(command.qualified_name.clone()); + } + } else { + flatten_commands(result, &command.subcommands, searching); + } + } + } + + let commands = &ctx.framework.options().commands; + let mut result = Vec::with_capacity(commands.len()); + + flatten_commands(&mut result, commands, searching); + + result.sort_by_key(|a| strsim::levenshtein(a, searching)); + result +} + +/// The help function from builtins copied. +pub async fn builtin_help( + ctx: crate::Context<'_>, + command: Option<&str>, + config: HelpConfiguration<'_>, +) -> Result<(), serenity::Error> { + match command { + Some(command) => help_single_command(ctx, command, config).await, + None => help_all_commands(ctx, config).await, + } +} + +/// Show the help menu. +#[cfg(not(tarpaulin_include))] +#[poise::command(category = "Utility", rename = "help", prefix_command, hide_in_help)] +pub async fn sub_help(ctx: Context<'_>) -> Result<(), Error> { + // This makes it possible to just make `help` a subcommand of any command + // `/fruit help` turns into `/help fruit` + // `/fruit help apple` turns into `/help fruit apple` + let parent = ctx + .parent_commands() + .iter() + .map(|&x| x.name.clone()) + .join(" "); + command_func(ctx, Some(&parent)).await +} + +use crate::Command; +#[allow(dead_code)] +enum HelpCommandMode<'a> { + Root, + Group(&'a Command), + Command(&'a Command), +} + +/// Shows the help menu. +#[poise::command( + prefix_command, + slash_command, + required_bot_permissions = "SEND_MESSAGES | EMBED_LINKS" +)] +async fn help( + ctx: Context<'_>, + #[rest] + #[description = "The command to get help with"] + #[autocomplete = "autocomplete"] + command: Option, +) -> Result<(), Error> { + command_func(ctx, command.as_deref()).await +} + +/// Wrapper around the help function. +pub async fn wrapper(ctx: Context<'_>) -> Result<(), Error> { + builtin_help( + ctx, + Some(ctx.command().name.as_str()), + poise::builtins::HelpConfiguration { + show_context_menu_commands: false, + show_subcommands: false, + extra_text_at_bottom: EXTRA_TEXT_AT_BOTTOM, + include_description: false, + ..Default::default() + }, + ) + .await?; + Ok(()) +} + +pub async fn command_func(ctx: Context<'_>, command: Option<&str>) -> Result<(), Error> { + let framework_options = ctx.framework().options(); + let commands = &framework_options.commands; + + let remaining_args: String; + let _mode = match command { + None => HelpCommandMode::Root, + Some(command) => { + let mut subcommand_iterator = command.split(' '); + + let top_level_command = subcommand_iterator.next().unwrap(); + let (mut command_obj, _, _) = require!( + poise::find_command(commands, top_level_command, true, &mut Vec::new()), + { + let msg = CrackedError::CommandNotFound(top_level_command.to_string()); + let _ = ctx.send_reply(msg.into(), true).await?; + Ok(()) + } + ); + + remaining_args = subcommand_iterator.collect(); + if !remaining_args.is_empty() { + (command_obj, _, _) = require!( + poise::find_command( + &command_obj.subcommands, + &remaining_args, + true, + &mut Vec::new() + ), + { + let group_name = command_obj.name.clone(); + let subcommand_name = remaining_args; + let msg = CrackedMessage::SubcommandNotFound { + group: group_name, + subcommand: subcommand_name, + }; + + let _ = send_reply(&ctx, msg, true).await?; + Ok(()) + } + ); + }; + + if command_obj.owners_only && !framework_options.owners.contains(&ctx.author().id) { + let msg = CrackedMessage::OwnersOnly; + let _ = send_reply(&ctx, msg, true).await?; + return Ok(()); + } + + if command_obj.subcommands.is_empty() { + HelpCommandMode::Command(command_obj) + } else { + HelpCommandMode::Group(command_obj) + } + }, + }; + + // // let neutral_colour = Colour::from_rgb(0x00, 0x00, 0x00); + // let neutral_colour = Colour::BLURPLE; + // let embed = CreateEmbed::default() + // .title("{command_name} Help!".to_string().replace( + // "{command_name}", + // &match &mode { + // HelpCommandMode::Root => ctx.cache().current_user().name.to_string(), + // HelpCommandMode::Group(c) | HelpCommandMode::Command(c) => { + // format!("`{}`", c.qualified_name) + // }, + // }, + // )) + // .description(match &mode { + // HelpCommandMode::Root => show_group_description(&get_command_mapping(commands)), + // HelpCommandMode::Command(command_obj) => { + // let mut msg = format!( + // "{}\n```/{}", + // command_obj + // .description + // .as_deref() + // .unwrap_or_else(|| "Command description not found!"), + // command_obj.qualified_name + // ); + + // format_params(&mut msg, command_obj); + // msg.push_str("```\n"); + + // if !command_obj.parameters.is_empty() { + // msg.push_str("__**Parameter Descriptions**__\n"); + // command_obj.parameters.iter().for_each(|p| { + // let name = &p.name; + // let description = + // p.description.as_deref().unwrap_or_else(|| "no description"); + // writeln!(msg, "`{name}`: {description}").unwrap(); + // }); + // }; + + // msg + // }, + // HelpCommandMode::Group(group) => show_group_description(&{ + // let mut map = IndexMap::new(); + // map.insert( + // group.qualified_name.as_ref(), + // group.subcommands.iter().collect(), + // ); + // map + // }), + // }) + // .colour(neutral_colour) + // .author( + // serenity::CreateEmbedAuthor::new(ctx.author().name.as_str()) + // .icon_url(ctx.author().face()), + // ) + // .footer(serenity::CreateEmbedFooter::new(match mode { + // HelpCommandMode::Group(c) => Cow::Owned( + // ctx.gettext("Use `/help {command_name} [command]` for more info on a command") + // .replace("{command_name}", &c.qualified_name), + // ), + // HelpCommandMode::Command(_) | HelpCommandMode::Root => { + // Cow::Borrowed(ctx.gettext("Use `/help [command]` for more info on a command")) + // }, + // })); + + use poise::builtins::HelpConfiguration; + + builtin_help( + ctx, + command, + HelpConfiguration { + show_context_menu_commands: false, + show_subcommands: false, + extra_text_at_bottom: EXTRA_TEXT_AT_BOTTOM, + ..Default::default() + }, + ) + .await?; + Ok(()) +} + +// Ok(()) + +// let neutral_colour = ctx.neutral_colour().await; +// let embed = CreateEmbed::default() +// .title(ctx.gettext("{command_name} Help!").replace( +// "{command_name}", +// &match &mode { +// HelpCommandMode::Root => ctx.cache().current_user().name.to_string(), +// HelpCommandMode::Group(c) | HelpCommandMode::Command(c) => { +// format!("`{}`", c.qualified_name) +// }, +// }, +// )) +// .description(match &mode { +// HelpCommandMode::Root => show_group_description(&get_command_mapping(commands)), +// HelpCommandMode::Command(command_obj) => { +// let mut msg = format!( +// "{}\n```/{}", +// command_obj +// .description +// .as_deref() +// .unwrap_or_else(|| ctx.gettext("Command description not found!")), +// command_obj.qualified_name +// ); + +// format_params(&mut msg, command_obj); +// msg.push_str("```\n"); + +// if !command_obj.parameters.is_empty() { +// msg.push_str(ctx.gettext("__**Parameter Descriptions**__\n")); +// command_obj.parameters.iter().for_each(|p| { +// let name = &p.name; +// let description = p +// .description +// .as_deref() +// .unwrap_or_else(|| ctx.gettext("no description")); +// writeln!(msg, "`{name}`: {description}").unwrap(); +// }); +// }; + +// msg +// }, +// HelpCommandMode::Group(group) => show_group_description(&{ +// let mut map = IndexMap::new(); +// map.insert( +// group.qualified_name.as_ref(), +// group.subcommands.iter().collect(), +// ); +// map +// }), +// }) +// .colour(neutral_colour) +// .author( +// serenity::CreateEmbedAuthor::new(ctx.author().name.as_str()) +// .icon_url(ctx.author().face()), +// ) +// .footer(serenity::CreateEmbedFooter::new(match mode { +// HelpCommandMode::Group(c) => Cow::Owned( +// ctx.gettext("Use `/help {command_name} [command]` for more info on a command") +// .replace("{command_name}", &c.qualified_name), +// ), +// HelpCommandMode::Command(_) | HelpCommandMode::Root => { +// Cow::Borrowed(ctx.gettext("Use `/help [command]` for more info on a command")) +// }, +// })); + +// ctx.send(poise::CreateReply::default().embed(embed)).await?; +// Ok(()) +//} + +// /set calls /help set +pub use command_func as command; +pub fn help_commands() -> [Command; 1] { + [help()] +} + +// Contains the built-in help command and surrounding infrastructure + +use poise::{serenity_prelude as serenity, CreateReply}; +use std::fmt::Write as _; + +/// Convenience function to align descriptions behind commands +struct TwoColumnList(Vec<(String, Option)>); + +impl TwoColumnList { + /// Creates a new [`TwoColumnList`] + fn new() -> Self { + Self(Vec::new()) + } + + /// Add a line that needs the padding between the columns + fn push_two_colums(&mut self, command: String, description: String) { + self.0.push((command, Some(description))); + } + + /// Add a line that doesn't influence the first columns's width + fn push_heading(&mut self, category: &str) { + if !self.0.is_empty() { + self.0.push(("".to_string(), None)); + } + let mut category = category.to_string(); + category += ":"; + self.0.push((category, None)); + } + + /// Convert the list into a string with aligned descriptions + fn into_string(self) -> String { + let longest_command = self + .0 + .iter() + .filter_map(|(command, description)| { + if description.is_some() { + Some(command.len()) + } else { + None + } + }) + .max() + .unwrap_or(0); + let mut text = String::new(); + for (command, description) in self.0 { + //let command = command.replace("_", r#"\\_"#); + if let Some(description) = description { + let padding = " ".repeat(longest_command - command.len() + 3); + writeln!(text, "{}{}{}", command, padding, description).unwrap(); + } else { + writeln!(text, "{}", command).unwrap(); + } + } + text + } +} + +/// Get the prefix from options +pub(super) async fn get_prefix_from_options(ctx: poise::Context<'_, U, E>) -> Option { + let options = &ctx.framework().options().prefix_options; + match &options.prefix { + Some(fixed_prefix) => Some(fixed_prefix.clone()), + None => match options.dynamic_prefix { + Some(dynamic_prefix_callback) => { + match dynamic_prefix_callback(poise::PartialContext::from(ctx)).await { + Ok(Some(dynamic_prefix)) => Some(dynamic_prefix), + _ => None, + } + }, + None => None, + }, + } +} + +/// Format context menu command name +fn format_context_menu_name(command: &crate::Command) -> Option { + let kind = match command.context_menu_action { + Some(poise::ContextMenuCommandAction::User(_)) => "user", + Some(poise::ContextMenuCommandAction::Message(_)) => "message", + Some(poise::ContextMenuCommandAction::__NonExhaustive) => unreachable!(), + None => return None, + }; + Some(format!( + "{} (on {})", + command + .context_menu_name + .as_deref() + .unwrap_or(&command.name), + kind + )) +} + +/// Code for printing help of a specific command (e.g. `~help my_command`) +async fn help_single_command( + ctx: crate::Context<'_>, + command_name: &str, + config: HelpConfiguration<'_>, +) -> Result<(), serenity::Error> { + let commands = &ctx.framework().options().commands; + // Try interpret the command name as a context menu command first + let mut command = commands.iter().find(|command| { + if let Some(context_menu_name) = &command.context_menu_name { + if context_menu_name.eq_ignore_ascii_case(command_name) { + return true; + } + } + false + }); + // Then interpret command name as a normal command (possibly nested subcommand) + if command.is_none() { + if let Some((c, _, _)) = poise::find_command(commands, command_name, true, &mut vec![]) { + command = Some(c); + } + } + + let reply = if let Some(command) = command { + let mut invocations = Vec::new(); + let mut subprefix = None; + if command.slash_action.is_some() { + invocations.push(format!("`/{}`", command.name)); + subprefix = Some(format!(" /{}", command.name)); + } + if command.prefix_action.is_some() { + let prefix = match get_prefix_from_options(ctx).await { + Some(prefix) => prefix, + // None can happen if the prefix is dynamic, and the callback + // fails due to help being invoked with slash or context menu + // commands. Not sure there's a better way to handle this. + None => String::from(""), + }; + invocations.push(format!("`{}{}`", prefix, command.name)); + if subprefix.is_none() { + subprefix = Some(format!(" {}{}", prefix, command.name)); + } + } + if command.context_menu_name.is_some() && command.context_menu_action.is_some() { + // Since command.context_menu_action is Some, this unwrap is safe + invocations.push(format_context_menu_name(command).unwrap()); + if subprefix.is_none() { + subprefix = Some(String::from(" ")); + } + } + // At least one of the three if blocks should have triggered + assert!(subprefix.is_some()); + assert!(!invocations.is_empty()); + let invocations = invocations.join("\n"); + + let mut text = match (&command.description, &command.help_text) { + (Some(description), Some(help_text)) => { + if config.include_description { + format!("{}\n\n{}", description, help_text) + } else { + help_text.clone() + } + }, + (Some(description), None) => description.to_owned(), + (None, Some(help_text)) => help_text.clone(), + (None, None) => "No help available".to_string(), + }; + if !command.parameters.is_empty() { + text += "\n\n```\nParameters:\n"; + let mut parameterlist = TwoColumnList::new(); + for parameter in &command.parameters { + let name = parameter.name.clone(); + let description = parameter.description.as_deref().unwrap_or(""); + let description = format!( + "({}) {}", + if parameter.required { + "required" + } else { + "optional" + }, + description, + ); + parameterlist.push_two_colums(name, description); + } + text += ¶meterlist.into_string(); + text += "```"; + } + if !command.subcommands.is_empty() { + text += "\n\n```\nSubcommands:\n"; + let mut commandlist = TwoColumnList::new(); + // Subcommands can exist on context menu commands, but there's no + // hierarchy in the menu, so just display them as a list without + // subprefix. + preformat_subcommands( + &mut commandlist, + command, + &subprefix.unwrap_or_else(|| String::from(" ")), + ); + text += &commandlist.into_string(); + text += "```"; + } + format!("**{}**\n\n{}", invocations, text) + } else { + format!("No such command `{}`", command_name) + }; + + if reply.len() > 1000 { + let bot_name = ctx.cache().current_user().name.clone(); + create_paged_embed(ctx, bot_name, "Help".to_string(), reply, 900).await?; + } else { + let create_reply = CreateReply::default() + .content(reply) + .ephemeral(config.ephemeral); + ctx.send(create_reply).await?; + } + + Ok(()) +} + +/// Recursively formats all subcommands +fn preformat_subcommands(commands: &mut TwoColumnList, command: &crate::Command, prefix: &str) { + let as_context_command = command.slash_action.is_none() && command.prefix_action.is_none(); + for subcommand in &command.subcommands { + let command = if as_context_command { + let name = format_context_menu_name(subcommand); + if name.is_none() { + continue; + }; + name.unwrap() + } else { + format!("{} {}", prefix, subcommand.name) + }; + let description = subcommand.description.as_deref().unwrap_or("").to_string(); + commands.push_two_colums(command, description); + // We could recurse here, but things can get cluttered quickly. + // Instead, we show (using this function) subsubcommands when + // the user asks for help on the subcommand. + } +} + +/// Preformat lines (except for padding,) like `(" /ping", "Emits a ping message")` +fn preformat_command( + commands: &mut TwoColumnList, + config: &HelpConfiguration<'_>, + command: &crate::Command, + indent: &str, + options_prefix: Option<&str>, +) { + let prefix = if command.slash_action.is_some() { + String::from("/") + } else if command.prefix_action.is_some() { + options_prefix.map(String::from).unwrap_or_default() + } else { + // This is not a prefix or slash command, i.e. probably a context menu only command + // This should have been filtered out in `generate_all_commands` + unreachable!(); + }; + + let prefix = format!("{}{}{}", indent, prefix, command.name); + commands.push_two_colums( + prefix.clone(), + command.description.as_deref().unwrap_or("").to_string(), + ); + if config.show_subcommands { + preformat_subcommands(commands, command, &prefix) + } +} + +/// Create help text for `help_all_commands` +/// +/// This is a separate function so we can have tests for it +async fn generate_all_commands( + ctx: crate::Context<'_>, + config: &HelpConfiguration<'_>, +) -> Result { + let mut categories = indexmap::IndexMap::, Vec<&crate::Command>>::new(); + for cmd in &ctx.framework().options().commands { + categories + .entry(cmd.category.as_deref()) + .or_default() + .push(cmd); + } + + let options_prefix = get_prefix_from_options(ctx).await; + + //let mut menu = String::from("```\n"); + let mut menu = String::from(""); + + let mut commandlist = TwoColumnList::new(); + for (category_name, commands) in categories { + let commands = commands + .into_iter() + .filter(|cmd| { + !cmd.hide_in_help && (cmd.prefix_action.is_some() || cmd.slash_action.is_some()) + }) + .collect::>(); + if commands.is_empty() { + continue; + } + commandlist.push_heading(category_name.unwrap_or("Commands")); + for command in commands { + preformat_command( + &mut commandlist, + config, + command, + " ", + options_prefix.as_deref(), + ); + } + } + menu += &commandlist.into_string(); + + if config.show_context_menu_commands { + menu += "\nContext menu commands:\n"; + + for command in &ctx.framework().options().commands { + let name = format_context_menu_name(command); + if name.is_none() { + continue; + }; + let _ = writeln!(menu, " {}", name.unwrap()); + } + } + + menu += "\n"; + menu += config.extra_text_at_bottom; + //menu += "\n```"; + + Ok(menu) +} + +/// Code for printing an overview of all commands (e.g. `~help`) +async fn help_all_commands( + ctx: crate::Context<'_>, + config: HelpConfiguration<'_>, +) -> Result<(), serenity::Error> { + let menu = generate_all_commands(ctx, &config).await?; + // let reply = CreateReply::default() + // .content(menu) + // .ephemeral(config.ephemeral); + + // ctx.send(reply).await?; + let bot_name = ctx.cache().current_user().name.clone(); + create_paged_embed(ctx, bot_name, "Help".to_string(), menu, 900).await?; + Ok(()) +} diff --git a/crack-core/src/commands/mod.rs b/crack-core/src/commands/mod.rs index 2bc82b54b..86616c604 100644 --- a/crack-core/src/commands/mod.rs +++ b/crack-core/src/commands/mod.rs @@ -1,33 +1,43 @@ pub mod admin; +#[cfg(feature = "crack-bf")] +pub mod bf; #[cfg(feature = "crack-gpt")] pub mod chatgpt; +pub mod help; pub mod music; pub mod music_utils; #[cfg(feature = "crack-osint")] pub mod osint; -pub mod ping; +pub mod permissions; pub mod playlist; pub mod settings; -pub mod version; +pub mod utility; pub use admin::*; +#[cfg(feature = "crack-bf")] +pub use bf::*; #[cfg(feature = "crack-gpt")] pub use chatgpt::*; +pub use help::sub_help; pub use music::*; pub use music_utils::*; #[cfg(feature = "crack-osint")] pub use osint::*; -pub use ping::*; -pub use playlist::playlist; -use serenity::all::Message; +pub use permissions::*; pub use settings::*; -pub use version::*; +pub use utility::*; + +pub mod register; +pub use register::*; pub use crate::errors::CrackedError; +use serenity::all::Message; pub type MessageResult = Result; pub type EmptyResult = Result<(), crate::Error>; +use crate::{Context, Error}; + pub trait ConvertToEmptyResult { fn convert(self) -> EmptyResult; } @@ -37,3 +47,88 @@ impl ConvertToEmptyResult for MessageResult { self.map(|_| ()).map_err(|e| e.into()) } } + +/// Return all the commands that are available in the bot. +pub fn all_commands() -> Vec { + vec![ + register(), + #[cfg(feature = "crack-bf")] + bf(), + #[cfg(feature = "crack-osint")] + osint(), + #[cfg(feature = "crack-gpt")] + chat(), + ] + .into_iter() + .chain(help::help_commands()) + .chain(music::music_commands()) + .chain(utility::utility_commands()) + .chain(settings::commands()) + // .chain(playlist::commands()) + // .chain(admin::admin_commands()) + .collect() +} + +pub fn all_command_names() -> Vec { + all_commands().into_iter().map(|c| c.name).collect() +} + +pub fn all_commands_map() -> dashmap::DashMap { + all_commands() + .into_iter() + .map(|c| (c.name.clone(), c)) + .collect::>() +} + +// use poise::serenity_prelude as serenity; +// /// Collects all commands into a [`Vec`] builder, which can be used +// /// to register the commands on Discord +// /// +// /// Also see [`register_application_commands_buttons`] for a ready to use register command +// /// +// /// ```rust,no_run +// /// # use poise::serenity_prelude as serenity; +// /// # async fn foo(ctx: poise::Context<'_, (), ()>) -> Result<(), serenity::Error> { +// /// let commands = &ctx.framework().options().commands; +// /// let create_commands = poise::builtins::create_application_commands_minus_help(commands); +// /// +// /// serenity::Command::set_global_commands(ctx, create_commands).await?; +// /// # Ok(()) } +// /// ``` +// pub fn create_application_commands_minus_help( +// commands: &[poise::Command], +// ) -> Vec { +// /// We decided to extract context menu commands recursively, despite the subcommand hierarchy +// /// not being preserved. Because it's more confusing to just silently discard context menu +// /// commands if they're not top-level commands. +// /// https://discord.com/channels/381880193251409931/919310428344029265/947970605985189989 +// fn recursively_add_context_menu_commands( +// builder: &mut Vec, +// command: &poise::Command, +// ) { +// if let Some(context_menu_command) = command.create_as_context_menu_command() { +// builder.push(context_menu_command); +// } +// for subcommand in &command.subcommands { +// if subcommand.name != "help" { +// recursively_add_context_menu_commands(builder, subcommand); +// } +// } +// } + +// let mut commands_builder = Vec::with_capacity(commands.len()); +// for command in commands { +// if let Some(slash_command) = command.create_as_slash_command() { +// commands_builder.push(slash_command); +// } +// recursively_add_context_menu_commands(&mut commands_builder, command); +// } +// commands_builder +// } + +/// Interactively register bot commands. +#[poise::command(prefix_command, hide_in_help = true)] +pub async fn register(ctx: Context<'_>) -> Result<(), Error> { + register_application_commands_buttons_cracked(ctx).await?; + Ok(()) +} diff --git a/crack-core/src/commands/music/autopause.rs b/crack-core/src/commands/music/autopause.rs index ad08eeb19..5334b640b 100644 --- a/crack-core/src/commands/music/autopause.rs +++ b/crack-core/src/commands/music/autopause.rs @@ -1,14 +1,23 @@ use crate::{ + commands::{cmd_check_music, sub_help as help}, errors::CrackedError, guild::operations::GuildSettingsOperations, + http_utils::SendMessageParams, messaging::message::CrackedMessage, - utils::{get_guild_name, send_response_poise}, + poise_ext::PoiseContextExt, Context, Error, }; /// Toggle autopause. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] +#[poise::command( + category = "Music", + slash_command, + prefix_command, + subcommands("help"), + guild_only, + check = "cmd_check_music" +)] pub async fn autopause(ctx: Context<'_>) -> Result<(), Error> { autopause_internal(ctx).await } @@ -17,24 +26,17 @@ pub async fn autopause(ctx: Context<'_>) -> Result<(), Error> { #[cfg(not(tarpaulin_include))] pub async fn autopause_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let prefix = ctx.data().bot_settings.get_prefix(); - let autopause = { - let name = get_guild_name(ctx.serenity_context(), guild_id); - let prefix = Some(prefix.as_str()); - let mut guild_settings = ctx - .data() - .get_or_create_guild_settings(guild_id, name, prefix) - .await; - guild_settings.toggle_autopause(); - guild_settings.autopause + let autopause = ctx.data().toggle_autopause(guild_id).await; + let params = SendMessageParams { + msg: if autopause { + CrackedMessage::AutopauseOn + } else { + CrackedMessage::AutopauseOff + }, + ..Default::default() }; - let msg = if autopause { - send_response_poise(ctx, CrackedMessage::AutopauseOn, true) - } else { - send_response_poise(ctx, CrackedMessage::AutopauseOff, true) - } - .await?; - ctx.data().add_msg_to_cache(guild_id, msg); + ctx.send_message(params).await?; + Ok(()) } diff --git a/crack-core/src/commands/music/autoplay.rs b/crack-core/src/commands/music/autoplay.rs index d8662dc50..5d586a9da 100644 --- a/crack-core/src/commands/music/autoplay.rs +++ b/crack-core/src/commands/music/autoplay.rs @@ -1,21 +1,37 @@ +use crate::commands::{cmd_check_music, sub_help as help}; use crate::guild::operations::GuildSettingsOperations; -use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crate::{messaging::message::CrackedMessage, utils::send_reply, Context, CrackedError, Error}; /// Toggle music autoplay. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only, aliases("ap"))] +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + guild_only, + aliases("ap"), + subcommands("help") +)] pub async fn autoplay(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + toggle_autoplay(ctx).await +} + +/// Toggle music autoplay. +pub async fn toggle_autoplay(ctx: Context<'_>) -> Result<(), Error> { + fn autoplay_msg(autoplay: bool) -> CrackedMessage { + if autoplay { + CrackedMessage::AutoplayOff + } else { + CrackedMessage::AutoplayOn + } + } + + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let autoplay = ctx.data().get_autoplay(guild_id).await; ctx.data().set_autoplay(guild_id, !autoplay).await; - let msg = if autoplay { - send_response_poise(ctx, CrackedMessage::AutoplayOff, true) - } else { - send_response_poise(ctx, CrackedMessage::AutoplayOn, true) - } - .await?; - ctx.data().add_msg_to_cache(guild_id, msg); + send_reply(&ctx, autoplay_msg(autoplay), true).await?; Ok(()) } diff --git a/crack-core/src/commands/music/clear.rs b/crack-core/src/commands/music/clear.rs index e6cabad0f..2c76e6d8d 100644 --- a/crack-core/src/commands/music/clear.rs +++ b/crack-core/src/commands/music/clear.rs @@ -1,18 +1,33 @@ use crate::{ + commands::{cmd_check_music, sub_help as help}, errors::{verify, CrackedError}, handlers::track_end::update_queue_messages, messaging::message::CrackedMessage, - utils::send_response_poise, + utils::send_reply, Context, Error, }; /// Clear the queue. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + prefix_command, + slash_command, + guild_only, + check = "cmd_check_music", + subcommands("help") +)] pub async fn clear(ctx: Context<'_>) -> Result<(), Error> { + clear_internal(ctx).await +} + +/// Clear the queue, internal. +pub async fn clear_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); + let manager = songbird::get(ctx.serenity_context()) + .await + .ok_or(CrackedError::NoSongbird)?; + let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; let handler = call.lock().await; let queue = handler.queue().current_queue(); @@ -27,7 +42,7 @@ pub async fn clear(ctx: Context<'_>) -> Result<(), Error> { let queue = handler.queue().current_queue(); drop(handler); - send_response_poise(ctx, CrackedMessage::Clear, true).await?; + send_reply(&ctx, CrackedMessage::Clear, true).await?; update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; Ok(()) } diff --git a/crack-core/src/commands/music/collector.rs b/crack-core/src/commands/music/collector.rs index fa5b5986e..7aefd9ccf 100644 --- a/crack-core/src/commands/music/collector.rs +++ b/crack-core/src/commands/music/collector.rs @@ -37,13 +37,13 @@ pub async fn boop(ctx: Context<'_>) -> Result<(), Error> { let mut msg = mci.message.clone(); msg.edit( - ctx, + &ctx, EditMessage::default().content(format!("Boop count: {}", boop_count)), ) .await?; mci.create_response( - ctx, + &ctx, CreateInteractionResponse::UpdateMessage(CreateInteractionResponseMessage::default()), ) .await?; diff --git a/crack-core/src/commands/music/doplay.rs b/crack-core/src/commands/music/doplay.rs index 583cbd7bc..716dd17bf 100644 --- a/crack-core/src/commands/music/doplay.rs +++ b/crack-core/src/commands/music/doplay.rs @@ -1,10 +1,11 @@ -use ::serenity::all::CommandInteraction; - use super::play_utils::query::QueryType; use super::play_utils::queue::{get_mode, get_msg, queue_track_back}; -use crate::commands::get_call_with_fail_msg; use crate::commands::play_utils::query::query_type_from_url; +use crate::commands::{cmd_check_music, sub_help as help}; use crate::sources::rusty_ytdl::RustyYoutubeClient; +use crate::CrackedResult; +use crate::{commands::get_call_or_join_author, http_utils::SendMessageParams}; +use ::serenity::all::CommandInteraction; //FIXME use crate::utils::edit_embed_response2; use crate::{ @@ -20,7 +21,7 @@ use crate::{ }, sources::spotify::SpotifyTrack, sources::youtube::build_query_aux_metadata, - utils::{get_human_readable_timestamp, get_track_metadata, send_embed_response_poise}, + utils::{get_human_readable_timestamp, get_track_metadata}, Context, Error, }; use ::serenity::{ @@ -52,33 +53,35 @@ pub enum Mode { /// Get the guild name. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + prefix_command, + slash_command, + guild_only, + check = "cmd_check_music", + subcommands("help") +)] pub async fn get_guild_name_info(ctx: Context<'_>) -> Result<(), Error> { - let _id = ctx.serenity_context().shard_id; + let shard_id = ctx.serenity_context().shard_id; ctx.say(format!( - "The name of this guild is: {}", - ctx.partial_guild().await.unwrap().name + "The name of this guild is: {}, shard_id: {}", + ctx.partial_guild().await.unwrap().name, + shard_id )) .await?; Ok(()) } -/// Sends the searching message after a play command is sent. -/// Also defers the interaction so we won't timeout. -#[cfg(not(tarpaulin_include))] -pub async fn send_search_message(ctx: Context<'_>) -> Result { - let embed = CreateEmbed::default().description(format!("{}", CrackedMessage::Search)); - send_embed_response_poise(ctx, embed).await -} - /// Play a song next #[cfg(not(tarpaulin_include))] #[poise::command( slash_command, prefix_command, guild_only, - aliases("next", "pn", "Pn", "insert", "ins", "push") + aliases("next", "pn", "Pn", "insert", "ins", "push"), + check = "cmd_check_music", + category = "Music" )] pub async fn playnext( ctx: Context<'_>, @@ -91,7 +94,14 @@ pub async fn playnext( /// Search interactively for a song #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only, aliases("s", "S"))] +#[poise::command( + slash_command, + prefix_command, + guild_only, + aliases("s", "S"), + check = "cmd_check_music", + category = "Music" +)] pub async fn search( ctx: Context<'_>, #[rest] @@ -103,14 +113,21 @@ pub async fn search( /// Play a song. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only, aliases("p", "P"))] +#[poise::command( + slash_command, + prefix_command, + guild_only, + aliases("p", "P"), + check = "cmd_check_music", + category = "Music" +)] pub async fn play( ctx: Context<'_>, #[rest] #[description = "song link or search query."] - query: Option, + query: String, ) -> Result<(), Error> { - play_internal(ctx, None, None, query).await + play_internal(ctx, None, None, Some(query)).await } /// Play a song with more options @@ -127,6 +144,9 @@ pub async fn optplay( play_internal(ctx, mode, file, query_or_url).await } +use crate::messaging::interface as msg_int; +use crate::poise_ext::PoiseContextExt; + /// Does the actual playing of the song, all the other commands use this. #[cfg(not(tarpaulin_include))] async fn play_internal( @@ -135,16 +155,21 @@ async fn play_internal( file: Option, query_or_url: Option, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + //let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; // FIXME: This should be generalized. let is_prefix = ctx.prefix() != "/"; let msg = get_msg(mode.clone(), query_or_url, is_prefix); if msg.is_none() && file.is_none() { - let embed = CreateEmbed::default() - .description(format!("{}", CrackedError::Other("No query provided!"))); - send_embed_response_poise(ctx, embed).await?; + // let embed = CreateEmbed::default().description(CrackedError::NoQuery.to_string()); + // send_embed_response_poise(&ctx, embed).await?; + let msg_params = SendMessageParams::default() + .with_channel(ctx.channel_id()) + .with_msg(CrackedMessage::CrackedError(CrackedError::NoQuery)) + .with_color(crate::serenity::Color::RED); + + ctx.send_message(msg_params).await?; return Ok(()); } @@ -159,16 +184,11 @@ async fn play_internal( tracing::warn!(target: "PLAY", "url: {}", url); - // reply with a temporary message while we fetch the source - // needed because interactions must be replied within 3s and queueing takes longer - let mut search_msg = send_search_message(ctx).await?; - - ctx.data().add_msg_to_cache(guild_id, search_msg.clone()); + let call = get_call_or_join_author(ctx).await?; + let mut search_msg = msg_int::send_search_message(&ctx).await?; tracing::debug!("search response msg: {:?}", search_msg); - let call = get_call_with_fail_msg(ctx).await?; - // determine whether this is a link or a query string let query_type = query_type_from_url(ctx, url, file).await?; @@ -190,27 +210,10 @@ async fn play_internal( return Ok(()); } - // FIXME: What was the point of this again? - // let _volume = { - // let mut settings = ctx.data().guild_settings_map.write().await; // .clone(); - // let guild_settings = settings.entry(guild_id).or_insert_with(|| { - // GuildSettings::new( - // guild_id, - // Some(prefix), - // get_guild_name(ctx.serenity_context(), guild_id), - // ) - // }); - // guild_settings.volume - // }; - - // let queue = call.lock().await.queue().current_queue().clone(); - // tracing::warn!("guild_settings: {:?}", guild_settings); // refetch the queue after modification // FIXME: I'm beginning to think that this walking of the queue is what's causing the performance issues. let handler = call.lock().await; let queue = handler.queue().current_queue(); - // queue.first().map(|t| t.set_volume(volume).unwrap()); - // queue.iter().for_each(|t| t.set_volume(volume).unwrap()); drop(handler); // This makes sense, we're getting the final response to the user based on whether @@ -220,7 +223,9 @@ async fn play_internal( // takes a long time. let embed = match queue.len().cmp(&1) { Ordering::Greater => { - let estimated_time = calculate_time_until_play(&queue, mode).await.unwrap(); + let estimated_time = calculate_time_until_play(&queue, mode) + .await + .unwrap_or_default(); match (query_type, mode) { ( @@ -245,7 +250,7 @@ async fn play_internal( y ); CreateEmbed::default() - .description(format!("{}", CrackedMessage::PlaylistQueued)) + .description(format!("{:?}", CrackedMessage::PlaylistQueued)) }, (QueryType::File(_x_), y) => { tracing::error!("QueryType::File, mode: {:?}", y); @@ -302,7 +307,7 @@ async fn match_mode<'a>( mode: Mode, query_type: QueryType, search_msg: &'a mut Message, -) -> Result { +) -> CrackedResult { tracing::info!("mode: {:?}", mode); match mode { @@ -326,7 +331,7 @@ async fn match_mode<'a>( pub fn check_banned_domains( guild_settings: &GuildSettings, query_type: Option, -) -> Result, CrackedError> { +) -> CrackedResult> { if let Some(QueryType::Keywords(_)) = query_type { if !guild_settings.allow_all_domains.unwrap_or(true) && (guild_settings.banned_domains.contains("youtube.com") @@ -337,7 +342,7 @@ pub fn check_banned_domains( // domain: "youtube.com".to_string(), // }; - // send_response_poise_text(ctx, message).await?; + // send_reply(&ctx, message).await?; // Ok(None) Err(CrackedError::Other("youtube.com is banned")) } else { @@ -508,7 +513,7 @@ async fn build_queued_embed( // let title_text = &format!("[**{}**]({})", meta_title, source_url); let footer_text = format!( - "{}{}\n{}{}", + "{} {}\n{} {}", TRACK_DURATION, get_human_readable_timestamp(metadata.duration), TRACK_TIME_TO_PLAY, @@ -531,12 +536,14 @@ pub async fn queue_aux_metadata( ctx: Context<'_>, aux_metadata: &[MyAuxMetadata], mut msg: Message, -) -> Result<(), CrackedError> { +) -> CrackedResult<()> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let search_results = aux_metadata; let client = &ctx.data().http_client; - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); + let manager = songbird::get(ctx.serenity_context()) + .await + .ok_or(CrackedError::NotConnected)?; let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; for metadata in search_results { @@ -545,7 +552,7 @@ pub async fn queue_aux_metadata( let search_query = build_query_aux_metadata(metadata.metadata()); let _ = msg .edit( - ctx, + &ctx, EditMessage::default().content(format!("Queuing... {}", search_query)), ) .await; diff --git a/crack-core/src/commands/music/dosearch.rs b/crack-core/src/commands/music/dosearch.rs index 6e402286f..b8e89bb46 100644 --- a/crack-core/src/commands/music/dosearch.rs +++ b/crack-core/src/commands/music/dosearch.rs @@ -1,5 +1,8 @@ use crate::{ - errors::CrackedError, messaging::interface::create_search_results_reply, Context, Error, + commands::{cmd_check_music, sub_help as help}, + errors::CrackedError, + messaging::interface::create_search_results_reply, + Context, Error, }; use poise::ReplyHandle; use reqwest::Client; @@ -8,7 +11,15 @@ use songbird::input::YoutubeDl; /// Search for a song and play it. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + prefix_command, + slash_command, + guild_only, + check = "cmd_check_music", + aliases("ytsearch"), + subcommands("help") +)] pub async fn do_yt_search( ctx: Context<'_>, #[rest] diff --git a/crack-core/src/commands/music/grab.rs b/crack-core/src/commands/music/grab.rs index e5d10d3ea..92997ac27 100644 --- a/crack-core/src/commands/music/grab.rs +++ b/crack-core/src/commands/music/grab.rs @@ -1,35 +1,36 @@ -use crate::utils::send_now_playing; -use crate::{errors::CrackedError, Context, Error}; +use crate::commands::help; +use crate::poise_ext::MessageInterfaceCtxExt; +use crate::{Context, Error}; -/// interface::create_now_playing_embed, /// Send the current tack to your DMs. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, aliases("save"), guild_only)] -pub async fn grab(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; - let channel = ctx - .author() - .create_dm_channel(&ctx.serenity_context().http) - .await?; - - let msg = send_now_playing( - channel.id, - ctx.serenity_context().http.clone(), - call.clone(), - None, - None, - ) - .await?; - - ctx.data().add_msg_to_cache(guild_id, msg); +#[poise::command( + category = "Music", + slash_command, + prefix_command, + aliases("save"), + guild_only +)] +pub async fn grab( + ctx: Context<'_>, + #[flag] + #[description = "Show the help menu."] + help: bool, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + grab_internal(ctx).await +} - let reply_handle = ctx.say("Sent you a DM with the current track").await?; +#[cfg(not(tarpaulin_include))] +/// Internal function for grab. +async fn grab_internal(ctx: Context<'_>) -> Result<(), Error> { + let chan_id = ctx.author().create_dm_channel(&ctx).await?.id; - let msg = reply_handle.into_message().await?; + ctx.send_now_playing(chan_id, None, None).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + ctx.send_grabbed_notice().await?; Ok(()) } diff --git a/crack-core/src/commands/music/help.rs b/crack-core/src/commands/music/help.rs deleted file mode 100644 index 672114b22..000000000 --- a/crack-core/src/commands/music/help.rs +++ /dev/null @@ -1,75 +0,0 @@ -use crate::{Context, Error}; - -// /// Show help message -// #[poise::command(prefix_command, track_edits, category = "Utility")] -// async fn help( -// ctx: Context<'_>, -// #[description = "Command to get help for"] -// #[rest] -// mut command: Option, -// ) -> Result<(), Error> { -// // This makes it possible to just make `help` a subcommand of any command -// // `/fruit help` turns into `/help fruit` -// // `/fruit help apple` turns into `/help fruit apple` -// if ctx.invoked_command_name() != "help" { -// command = match command { -// Some(c) => Some(format!("{} {}", ctx.invoked_command_name(), c)), -// None => Some(ctx.invoked_command_name().to_string()), -// }; -// } -// let extra_text_at_bottom = "\ -// Type `?help command` for more info on a command. -// You can edit your `?help` message to the bot and the bot will edit its response."; - -// let config = HelpConfiguration { -// show_subcommands: true, -// show_context_menu_commands: true, -// ephemeral: true, -// extra_text_at_bottom, - -// ..Default::default() -// }; -// poise::builtins::help(ctx, command.as_deref(), config).await?; -// Ok(()) -// } - -/// Show the help menu. -#[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, track_edits, slash_command, category = "Utility")] -pub async fn help( - ctx: Context<'_>, - #[description = "Specific command to show help about"] - // #[autocomplete = "poise::builtins::autocomplete_command"] - #[rest] - mut command: Option, -) -> Result<(), Error> { - // This makes it possible to just make `help` a subcommand of any command - // `/fruit help` turns into `/help fruit` - // `/fruit help apple` turns into `/help fruit apple` - if ctx.invoked_command_name() != "help" { - command = match command { - Some(c) => Some(format!("{} {}", ctx.invoked_command_name(), c)), - None => Some(ctx.invoked_command_name().to_string()), - }; - } - poise::builtins::help( - ctx, - command.as_deref(), - poise::builtins::HelpConfiguration { - show_subcommands: false, - show_context_menu_commands: false, - extra_text_at_bottom: "This is a friendly crack smoking parrot that plays music.", - ..Default::default() - }, - ) - .await?; - Ok(()) -} - -/// Get information about the servers this bot is in. -#[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, owners_only)] -pub async fn servers(ctx: Context<'_>) -> Result<(), Error> { - poise::builtins::servers(ctx).await?; - Ok(()) -} diff --git a/crack-core/src/commands/music/invite.rs b/crack-core/src/commands/music/invite.rs deleted file mode 100644 index a851541a8..000000000 --- a/crack-core/src/commands/music/invite.rs +++ /dev/null @@ -1,25 +0,0 @@ -use crate::{ - messaging::messages::{INVITE_LINK_TEXT, INVITE_TEXT, INVITE_URL}, - Context, Error, -}; -use poise::serenity_prelude::GuildId; - -/// Vote link for cracktunes on top.gg -#[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command)] -pub async fn invite(ctx: Context<'_>) -> Result<(), Error> { - let guild_id: Option = ctx.guild_id(); - - let reply_handle = ctx - .reply(format!( - "{} [{}]({})", - INVITE_TEXT, INVITE_LINK_TEXT, INVITE_URL - )) - .await?; - - let msg = reply_handle.into_message().await?; - - guild_id.map(|id| ctx.data().add_msg_to_cache(id, msg)); - - Ok(()) -} diff --git a/crack-core/src/commands/music/leave.rs b/crack-core/src/commands/music/leave.rs index d2f20c482..d53bf1e3b 100644 --- a/crack-core/src/commands/music/leave.rs +++ b/crack-core/src/commands/music/leave.rs @@ -1,18 +1,32 @@ use crate::{ - errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, - Error, + commands::{cmd_check_music, help}, + errors::CrackedError, + messaging::message::CrackedMessage, + utils::send_reply, + Context, Error, }; use songbird::error::JoinError; -/// Leave a voice channel. +/// Tell the bot to leave the voice channel it is in. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Music", prefix_command, slash_command, guild_only, - aliases("dc", "fuckoff", "fuck off") + aliases("dc", "fuckoff", "fuck off"), + //subcommands("help"), + check = "cmd_check_music" )] -pub async fn leave(ctx: Context<'_>) -> Result<(), Error> { +pub async fn leave( + ctx: Context<'_>, + #[flag] + #[description = "Show a help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } leave_internal(ctx).await } @@ -37,7 +51,6 @@ pub async fn leave_internal(ctx: Context<'_>) -> Result<(), Error> { }, }; - let msg = send_response_poise(ctx, crack_msg, true).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_reply(&ctx, crack_msg, true).await?; Ok(()) } diff --git a/crack-core/src/commands/music/lyrics.rs b/crack-core/src/commands/music/lyrics.rs index c66cdf7ce..b0c08e229 100644 --- a/crack-core/src/commands/music/lyrics.rs +++ b/crack-core/src/commands/music/lyrics.rs @@ -1,24 +1,34 @@ use crate::{ - commands::MyAuxMetadata, errors::CrackedError, http_utils, - messaging::interface::create_lyrics_embed, Context, ContextExt, Error, + commands::{cmd_check_music, help, MyAuxMetadata}, + errors::CrackedError, + http_utils, + messaging::interface::create_lyrics_embed, + poise_ext::ContextExt, + Context, Error, }; -#[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] /// Search for song lyrics. +#[cfg(not(tarpaulin_include))] +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only +)] pub async fn lyrics( ctx: Context<'_>, + #[flag] + #[description = "Show a help menu for this command."] + help: bool, #[rest] - #[description = "The query to search for"] + #[description = "The search query."] query: Option, ) -> Result<(), Error> { - let query = query_or_title(ctx, query).await?; - tracing::warn!("searching for lyrics for {}", query); - - let client = lyric_finder::Client::from_http_client(http_utils::get_client()); - - let res = client.get_lyric(&query).await?; - create_lyrics_embed(ctx, res).await.map_err(Into::into) + if help { + return help::wrapper(ctx).await; + } + lyrics_internal(ctx, query).await } #[cfg(not(tarpaulin_include))] @@ -56,119 +66,3 @@ pub async fn query_or_title(ctx: Context<'_>, query: Option) -> Result Result<(String, String, String), Error> { -// let (track, artists, lyric) = match client.get_lyric(&query).await { -// Ok(lyric_finder::LyricResult::Some { -// track, -// artists, -// lyric, -// }) => { -// tracing::warn!("{} by {}'s lyric:\n{}", track, artists, lyric); -// (track, artists, lyric) -// }, -// Ok(lyric_finder::LyricResult::None) => { -// tracing::error!("lyric not found! query: {}", query); -// ( -// "Unknown".to_string(), -// "Unknown".to_string(), -// "Lyric not found!".to_string(), -// ) -// }, -// Err(e) => { -// tracing::error!("lyric query error: {}", e); -// return Err(e.into()); -// }, -// }; - -// Ok((track, artists, lyric)) -// } - -// #[cfg(test)] -// mod test { -// use super::*; - -// // // Mock your dependencies here -// // // For example, a mock `lyric_finder::Client` might look like this -// // mock! { -// // LyricFinderClient{} - -// // #[async_trait] -// // impl LyricFinderClient for LyricFinderClient { -// // async fn get_lyric(&self, query: &str) -> anyhow::Result; -// // } -// // } - -// #[tokio::test] -// async fn test_do_lyric_query_not_found() { -// let client = lyric_finder::Client::new(); -// let result = do_lyric_query(client, "Hit That The Offpspring".to_string()).await; -// match result { -// Ok((track, artists, lyric)) => { -// assert_eq!(track, "Hit That"); -// assert_eq!(artists, "The Offspring"); -// assert_ne!(lyric, "Lyric not found!"); -// }, -// Err(_) => panic!("Unexpected error"), -// } -// } - -// // #[tokio::test] -// // async fn test_query_or_title_with_query() { -// // // Setup the test context and other necessary mock objects -// // let ctx = ...; // Mocked context -// // let query = Some("Some query".to_string()); - -// // // Perform the test -// // let result = query_or_title(ctx, query).await; - -// // // Assert the outcome -// // assert_eq!(result.unwrap(), "Some query"); -// // } - -// // #[tokio::test] -// // async fn test_query_or_title_without_query() { -// // // Setup the test context and other necessary mock objects -// // // let ctx = ...; // Mocked context without a current track -// // let ctx = poise::ApplicationContext:: - -// // // Perform the test -// // let result = query_or_title(ctx, None).await; - -// // // Assert that an error is returned because there's no current track -// // assert!(result.is_err()); -// // } - -// // #[tokio::test] -// // async fn test_do_lyric_query_found() { -// // // Setup the mocked `lyric_finder::Client` -// // let mut mock_client = MockLyricFinderClient::new(); -// // mock_client -// // .expect_get_lyric() -// // .with(eq("Some query")) -// // .times(1) -// // .return_once(|_| { -// // Ok(lyric_finder::LyricResult::Some { -// // track: "Some track".to_string(), -// // artists: "Some artist".to_string(), -// // lyric: "Some lyrics".to_string(), -// // }) -// // }); - -// // // Perform the test -// // let result = do_lyric_query(mock_client, "Some query".to_string()).await; - -// // // Assert the outcome -// // assert_eq!( -// // result.unwrap(), -// // ( -// // "Some track".to_string(), -// // "Some artist".to_string(), -// // "Some lyrics".to_string() -// // ) -// // ); -// // } -// } diff --git a/crack-core/src/commands/music/manage_sources.rs b/crack-core/src/commands/music/manage_sources.rs index 669d1af2c..c29905564 100644 --- a/crack-core/src/commands/music/manage_sources.rs +++ b/crack-core/src/commands/music/manage_sources.rs @@ -31,13 +31,10 @@ pub async fn allow(ctx: Context<'_>) -> Result<(), Error> { use ::serenity::builder::{CreateInteractionResponse, CreateModal}; use crate::utils::get_guild_name; - let guild_settings = settings.entry(guild_id).or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(&prefix), - get_guild_name(ctx.serenity_context(), guild_id), - ) - }); + let name = get_guild_name(ctx.serenity_context(), guild_id).await; + let guild_settings = settings + .entry(guild_id) + .or_insert_with(|| GuildSettings::new(guild_id, Some(&prefix), name)); // transform the domain sets from the settings into a string let allowed_str = guild_settings diff --git a/crack-core/src/commands/music/mod.rs b/crack-core/src/commands/music/mod.rs index 8b1569f3a..5bcfa42e7 100644 --- a/crack-core/src/commands/music/mod.rs +++ b/crack-core/src/commands/music/mod.rs @@ -1,14 +1,11 @@ pub mod autopause; pub mod autoplay; -pub mod clean; pub mod clear; pub mod collector; pub mod doplay; pub mod dosearch; pub mod gambling; pub mod grab; -pub mod help; -pub mod invite; pub mod leave; pub mod lyrics; pub mod manage_sources; @@ -31,14 +28,11 @@ pub mod voteskip; pub use autopause::*; pub use autoplay::*; -pub use clean::*; pub use clear::*; pub use collector::*; pub use doplay::*; pub use gambling::*; pub use grab::*; -pub use help::*; -pub use invite::*; pub use leave::*; pub use lyrics::*; pub use manage_sources::*; @@ -57,3 +51,32 @@ pub use summon::*; pub use volume::*; pub use vote::*; pub use voteskip::*; + +pub fn music_commands() -> [crate::Command; 24] { + [ + autopause(), + autoplay(), + clear(), + grab(), + leave(), + lyrics(), + nowplaying(), + pause(), + play(), + playlog(), + playnext(), + queue(), + remove(), + repeat(), + resume(), + seek(), + shuffle(), + skip(), + stop(), + summon::summon(), + summonchannel(), + volume(), + vote(), + voteskip(), + ] +} diff --git a/crack-core/src/commands/music/nowplaying.rs b/crack-core/src/commands/music/nowplaying.rs index 165256832..0ae84eab4 100644 --- a/crack-core/src/commands/music/nowplaying.rs +++ b/crack-core/src/commands/music/nowplaying.rs @@ -1,17 +1,37 @@ +use crate::poise_ext::ContextExt; use crate::{ - errors::CrackedError, messaging::interface::create_now_playing_embed, - utils::send_embed_response_poise, Context, Error, + commands::{cmd_check_music, help}, + errors::CrackedError, + messaging::interface::create_now_playing_embed, + utils::send_embed_response_poise, + Context, Error, }; -use serenity::all::GuildId; -use serenity::prelude::Mutex; -use songbird::{Call, Songbird}; -use std::sync::Arc; /// Get the currently playing track. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only, aliases("np"))] -pub async fn nowplaying(ctx: Context<'_>) -> Result<(), Error> { - let (guild_id, _manager, call) = get_guild_id_and_songbird_call(ctx).await?; +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only, + aliases("np") +)] +pub async fn nowplaying( + ctx: Context<'_>, + #[flag] + #[description = "Show a help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + nowplaying_internal(ctx).await +} + +/// Get the currently playing track. Internal function. +pub async fn nowplaying_internal(ctx: Context<'_>) -> Result<(), Error> { + let call = ctx.get_call().await?; let handler = call.lock().await; let track = handler @@ -20,20 +40,6 @@ pub async fn nowplaying(ctx: Context<'_>) -> Result<(), Error> { .ok_or(CrackedError::NothingPlaying)?; let embed = create_now_playing_embed(&track).await; - let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_embed_response_poise(&ctx, embed).await?; Ok(()) } - -/// Gets the guild id and songbird manager and call structs. -#[cfg(not(tarpaulin_include))] -pub async fn get_guild_id_and_songbird_call( - ctx: Context<'_>, -) -> Result<(GuildId, Arc, Arc>), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let manager = songbird::get(ctx.serenity_context()) - .await - .ok_or(CrackedError::Other("Songbird manager not found."))?; - let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; - Ok((guild_id, manager, call)) -} diff --git a/crack-core/src/commands/music/pause.rs b/crack-core/src/commands/music/pause.rs index e7a27d89c..7f43e8666 100644 --- a/crack-core/src/commands/music/pause.rs +++ b/crack-core/src/commands/music/pause.rs @@ -1,30 +1,27 @@ use crate::{ + commands::cmd_check_music, errors::{verify, CrackedError}, messaging::message::CrackedMessage, - utils::send_response_poise_text, + poise_ext::ContextExt, + utils::send_reply, {Context, Error}, }; /// Pause the current track. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] -pub async fn pause( - ctx: Context<'_>, - #[description = "Pause the current track"] send_reply: Option, -) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); - - let handler = call.lock().await; - let queue = handler.queue(); +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + guild_only +)] +pub async fn pause(ctx: Context<'_>) -> Result<(), Error> { + let queue = ctx.get_queue().await?; verify(!queue.is_empty(), CrackedError::NothingPlaying)?; verify(queue.pause(), CrackedError::Other("Failed to pause"))?; - if send_reply.unwrap_or(true) { - let msg = send_response_poise_text(ctx, CrackedMessage::Pause).await?; - ctx.data().add_msg_to_cache(guild_id, msg); - } + send_reply(&ctx, CrackedMessage::Pause, true).await?; Ok(()) } diff --git a/crack-core/src/commands/music/play_utils/query.rs b/crack-core/src/commands/music/play_utils/query.rs index 6d5be8b83..e1cdc182b 100644 --- a/crack-core/src/commands/music/play_utils/query.rs +++ b/crack-core/src/commands/music/play_utils/query.rs @@ -1,6 +1,9 @@ use super::queue::{queue_track_back, queue_track_front}; use super::{queue_keyword_list_back, queue_query_list_offset}; use crate::guild::operations::GuildSettingsOperations; +use crate::messaging::interface::create_search_response; +use crate::sources::youtube::video_info_to_source_and_metadata; +use crate::CrackedResult; use crate::{ commands::{check_banned_domains, MyAuxMetadata}, errors::{verify, CrackedError}, @@ -13,12 +16,8 @@ use crate::{ sources::{ rusty_ytdl::RustyYoutubeClient, spotify::{Spotify, SpotifyTrack, SPOTIFY}, - youtube::{ - search_query_to_source_and_metadata_rusty, search_query_to_source_and_metadata_ytdl, - video_info_to_source_and_metadata, - }, }, - utils::{edit_response_poise, send_search_response, yt_search_select}, + utils::{edit_response_poise, yt_search_select}, Context, Error, }; use ::serenity::all::{Attachment, CreateAttachment, CreateMessage}; @@ -297,7 +296,7 @@ impl QueryType { QueryType::YoutubeSearch(query) => { self.mode_search_keywords(ctx, call, query.clone()).await }, - _ => send_search_failed(ctx).await.map(|_| Vec::new()), + _ => send_search_failed(&ctx).await.map(|_| Vec::new()), } } @@ -319,7 +318,7 @@ impl QueryType { ) .await?; queue_track_back(ctx, &call, &qt).await - // update_queue_messages(&ctx, ctx.data(), &queue, guild_id).await + // update_queue_messages(ctx, ctx.data(), &queue, guild_id).await } pub async fn mode_next( @@ -393,7 +392,7 @@ impl QueryType { .search(None) .await?; let user_id = ctx.author().id; - send_search_response(ctx, guild_id, user_id, query.clone(), res).await?; + create_search_response(&ctx, guild_id, user_id, query.clone(), res).await?; Ok(true) }, QueryType::Keywords(_) | QueryType::VideoLink(_) | QueryType::NewYoutubeDl(_) => { @@ -441,7 +440,7 @@ impl QueryType { // update_queue_messages(ctx.http(), ctx.data(), &queue, guild_id).await; Ok(true) }, - QueryType::None => send_no_query_provided(ctx).await.map(|_| false), + QueryType::None => send_no_query_provided(&ctx).await.map(|_| false), } } @@ -477,7 +476,7 @@ impl QueryType { }, _ => { ctx.defer().await?; // Why did I do this? - edit_response_poise(ctx, CrackedMessage::PlayAllFailed).await?; + edit_response_poise(&ctx, CrackedMessage::PlayAllFailed).await?; Ok(false) }, } @@ -576,11 +575,9 @@ impl QueryType { // } } - // FIXME: Do you want to have a reqwest client we keep around and pass into - // this instead of creating a new one every time? pub async fn get_track_source_and_metadata( &self, - ) -> Result<(SongbirdInput, Vec), CrackedError> { + ) -> CrackedResult<(SongbirdInput, Vec)> { use colored::Colorize; let client = http_utils::get_client().clone(); tracing::warn!("{}", format!("query_type: {:?}", self).red()); @@ -599,27 +596,18 @@ impl QueryType { QueryType::VideoLink(query) => { tracing::warn!("In VideoLink"); video_info_to_source_and_metadata(client.clone(), query.clone()).await - // let mut ytdl = YoutubeDl::new(client, query); - // tracing::warn!("ytdl: {:?}", ytdl); + // let mut ytdl = YoutubeDl::new(client, query.clone()); // let metadata = ytdl.aux_metadata().await?; // let my_metadata = MyAuxMetadata::Data(metadata); // Ok((ytdl.into(), vec![my_metadata])) }, QueryType::Keywords(query) => { tracing::warn!("In Keywords"); - let res = search_query_to_source_and_metadata_rusty( - client.clone(), - QueryType::Keywords(query.clone()), - ) - .await; - match res { - Ok((input, metadata)) => Ok((input, metadata)), - Err(_) => { - tracing::error!("falling back to ytdl!"); - search_query_to_source_and_metadata_ytdl(client.clone(), query.clone()) - .await - }, - } + // video_info_to_source_and_metadata(client.clone(), query.clone()).await + let mut ytdl = YoutubeDl::new_search(client, query.clone()); + let metadata = ytdl.aux_metadata().await?; + let my_metadata = MyAuxMetadata::Data(metadata); + Ok((ytdl.into(), vec![my_metadata])) }, QueryType::File(file) => { tracing::warn!("In File"); diff --git a/crack-core/src/commands/music/play_utils/queue.rs b/crack-core/src/commands/music/play_utils/queue.rs index a042e08f8..55606c113 100644 --- a/crack-core/src/commands/music/play_utils/queue.rs +++ b/crack-core/src/commands/music/play_utils/queue.rs @@ -1,19 +1,18 @@ use super::QueryType; use crate::{ commands::{Mode, MyAuxMetadata, RequestingUser}, - db::{aux_metadata_to_db_structures, Metadata, PlayLog, User}, errors::{verify, CrackedError}, handlers::track_end::update_queue_messages, http_utils::CacheHttpExt, - Context as CrackContext, ContextExt, Error, + poise_ext::ContextExt, + Context as CrackContext, Error, }; -use serenity::all::{CacheHttp, ChannelId, CreateEmbed, EditMessage, GuildId, Message, UserId}; +use serenity::all::{CreateEmbed, EditMessage, Message, UserId}; use songbird::{ - input::{AuxMetadata, Input as SongbirdInput}, + input::Input as SongbirdInput, tracks::{Track, TrackHandle}, Call, }; -use sqlx::PgPool; use std::sync::Arc; use tokio::sync::Mutex; @@ -25,6 +24,26 @@ pub struct TrackReadyData { pub username: String, } +/// Takes a query and returns a track that is ready to be played, along with relevant metadata. +pub async fn ready_query2(query_type: QueryType) -> Result { + let (source, metadata_vec): (SongbirdInput, Vec) = + query_type.get_track_source_and_metadata().await?; + let metadata = match metadata_vec.first() { + Some(x) => x.clone(), + None => { + return Err(CrackedError::Other("metadata.first() failed")); + }, + }; + let track: Track = source.into(); + + Ok(TrackReadyData { + track, + metadata, + user_id: UserId::new(1), + username: "auto".to_string(), + }) +} + /// Takes a query and returns a track that is ready to be played, along with relevant metadata. pub async fn ready_query( ctx: CrackContext<'_>, @@ -59,6 +78,13 @@ pub async fn queue_track_ready_front( let mut handler = call.lock().await; let track_handle = handler.enqueue(ready_track.track).await; let new_q = handler.queue().current_queue(); + // Zeroth index: Currently playing track + // First index: Current next track + // Second index onward: Tracks to be played, we get in here most likely, + // but if we're in one of the first two we don't want to do anything. + if new_q.len() < 3 { + return Ok(new_q); + } handler.queue().modify_queue(|queue| { let back = queue.pop_back().unwrap(); queue.insert(1, back); @@ -158,7 +184,7 @@ pub async fn queue_keyword_list_back<'a>( .collect::>() .join("\n"); msg.edit( - ctx, + &ctx, EditMessage::new().embed(CreateEmbed::default().description(format!( "Queuing {} songs... \n{}", chunk.len(), @@ -251,117 +277,6 @@ pub async fn queue_query_list_offset<'a>( Ok(()) } -/// Writes metadata to the database for a playing track. -#[cfg(not(tarpaulin_include))] -pub async fn write_metadata_pg( - database_pool: &PgPool, - aux_metadata: AuxMetadata, - user_id: UserId, - username: String, - guild_id: GuildId, - channel_id: ChannelId, -) -> Result { - let returned_metadata = { - let (metadata, _playlist_track) = match aux_metadata_to_db_structures( - &aux_metadata, - guild_id.get() as i64, - channel_id.get() as i64, - ) { - Ok(x) => x, - Err(e) => { - tracing::error!("aux_metadata_to_db_structures error: {}", e); - return Err(CrackedError::Other("aux_metadata_to_db_structures error")); - }, - }; - let updated_metadata = - match crate::db::metadata::Metadata::get_or_create(database_pool, &metadata).await { - Ok(x) => x, - Err(e) => { - tracing::error!("crate::db::metadata::Metadata::create error: {}", e); - metadata.clone() - }, - }; - - match User::insert_or_update_user(database_pool, user_id.get() as i64, username).await { - Ok(_) => { - tracing::info!("Users::insert_or_update"); - }, - Err(e) => { - tracing::error!("Users::insert_or_update error: {}", e); - }, - }; - match PlayLog::create( - database_pool, - user_id.get() as i64, - guild_id.get() as i64, - updated_metadata.id as i64, - ) - .await - { - Ok(x) => { - tracing::info!("PlayLog::create: {:?}", x); - }, - Err(e) => { - tracing::error!("PlayLog::create error: {}", e); - }, - }; - metadata - }; - Ok(returned_metadata) -} - -/// Enqueues a track and adds metadata to the database. (parameters broken out) -// TODO: This is redundant with the other queuing functions. Remove it. -#[cfg(not(tarpaulin_include))] -pub async fn enqueue_track_pgwrite_asdf( - database_pool: &PgPool, - guild_id: GuildId, - channel_id: ChannelId, - user_id: UserId, - cache_http: impl CacheHttp, - call: &Arc>, - query_type: &QueryType, -) -> Result, CrackedError> { - // use crate::sources::youtube::get_track_source_and_metadata; - - tracing::info!("query_type: {:?}", query_type); - // is this comment still relevant to this section of code? - // safeguard against ytdl dying on a private/deleted video and killing the playlist - let (source, metadata): (SongbirdInput, Vec) = - query_type.get_track_source_and_metadata().await?; - let res = match metadata.first() { - Some(x) => x.clone(), - None => { - return Err(CrackedError::Other("metadata.first() failed")); - }, - }; - let track: Track = source.into(); - - let username = cache_http.user_id_to_username_or_default(user_id); - - let MyAuxMetadata::Data(aux_metadata) = res.clone(); - - let returned_metadata = write_metadata_pg( - database_pool, - aux_metadata, - user_id, - username, - guild_id, - channel_id, - ) - .await?; - - tracing::info!("returned_metadata: {:?}", returned_metadata); - - let mut handler = call.lock().await; - let track_handle = handler.enqueue(track).await; - let mut map = track_handle.typemap().write().await; - map.insert::(res.clone()); - map.insert::(RequestingUser::UserId(user_id)); - - Ok(handler.queue().current_queue()) -} - /// Get the play mode and the message from the parameters to the play command. // TODO: There is a lot of cruft in this from the older version of this. Clean it up. pub fn get_mode(is_prefix: bool, msg: Option, mode: Option) -> (Mode, String) { diff --git a/crack-core/src/commands/music/playlog.rs b/crack-core/src/commands/music/playlog.rs index 51e67e842..6af30bc52 100644 --- a/crack-core/src/commands/music/playlog.rs +++ b/crack-core/src/commands/music/playlog.rs @@ -1,29 +1,36 @@ -use crate::db::PlayLog; +use crate::commands::cmd_check_music; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; -use crate::{Context, Error}; +use crate::utils::{create_paged_embed, send_reply}; +use crate::{poise_ext::ContextExt, Context, Error}; /// Get recently played tracks form the guild. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + guild_only +)] pub async fn playlog(ctx: Context<'_>) -> Result<(), Error> { - playlog_(ctx).await + playlog_internal(ctx).await } /// Get recently played tracks for the guild. #[cfg(not(tarpaulin_include))] -pub async fn playlog_(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - - let last_played = PlayLog::get_last_played( - ctx.data().database_pool.as_ref().unwrap(), - None, - Some(guild_id.get() as i64), +pub async fn playlog_internal(ctx: Context<'_>) -> Result<(), Error> { + let last_played = ctx.get_last_played().await?; + let last_played_str = last_played.join("\n"); + + create_paged_embed( + ctx, + ctx.author().name.clone(), + "Playlog".to_string(), + last_played_str, + 756, ) .await?; - - let msg = send_response_poise(ctx, CrackedMessage::PlayLog(last_played), true).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + // let _ = send_reply(&ctx, CrackedMessage::PlayLog(last_played), true).await?; Ok(()) } @@ -35,20 +42,15 @@ pub async fn myplaylog(ctx: Context<'_>) -> Result<(), Error> { myplaylog_(ctx).await } +// use crate::commands::CrackedError; #[cfg(not(tarpaulin_include))] pub async fn myplaylog_(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + // let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let user_id = ctx.author().id; - let last_played = PlayLog::get_last_played( - ctx.data().database_pool.as_ref().unwrap(), - Some(user_id.get() as i64), - Some(guild_id.get() as i64), - ) - .await?; + let last_played = ctx.get_last_played_by_user(user_id).await?; - let msg = send_response_poise(ctx, CrackedMessage::PlayLog(last_played), true).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_reply(&ctx, CrackedMessage::PlayLog(last_played), true).await?; Ok(()) } diff --git a/crack-core/src/commands/music/queue.rs b/crack-core/src/commands/music/queue.rs index 72641dc38..827b89de5 100644 --- a/crack-core/src/commands/music/queue.rs +++ b/crack-core/src/commands/music/queue.rs @@ -1,8 +1,12 @@ +use crate::utils::get_interaction_new; use crate::{ + commands::cmd_check_music, errors::CrackedError, handlers::track_end::ModifyQueueHandler, - messaging::interface::{create_nav_btns, create_queue_embed}, - messaging::messages::QUEUE_EXPIRED, + messaging::{ + interface::{create_nav_btns, create_queue_embed}, + messages::QUEUE_EXPIRED, + }, utils::{calculate_num_pages, forget_queue_message}, Context, Error, }; @@ -19,21 +23,35 @@ const EMBED_TIMEOUT: u64 = 3600; /// Display the current queue. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, aliases("list", "q"), guild_only)] -pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { - use crate::utils::get_interaction_new; +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + aliases("list", "q"), + guild_only +)] +pub async fn queue( + ctx: Context<'_>, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } + queue_internal(ctx).await +} - tracing::info!("queue called"); +/// Internal queue function. +#[cfg(not(tarpaulin_include))] +pub async fn queue_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - tracing::info!("guild_id: {}", guild_id); let manager = songbird::get(ctx.serenity_context()) .await .ok_or(CrackedError::NotConnected)?; - tracing::trace!("manager: {:?}", manager); let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; - tracing::trace!("call: {:?}", call); - // FIXME let handler = call.lock().await; let tracks = handler.queue().current_queue(); @@ -43,7 +61,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { let num_pages = calculate_num_pages(&tracks); tracing::info!("num_pages: {}", num_pages); - let mut message = match get_interaction_new(ctx) { + let mut message = match get_interaction_new(&ctx) { Some(crate::utils::CommandOrMessageInteraction::Command(interaction)) => { interaction .create_response( @@ -71,7 +89,7 @@ pub async fn queue(ctx: Context<'_>) -> Result<(), Error> { }, }; - ctx.data().add_msg_to_cache(guild_id, message.clone()); + ctx.data().add_msg_to_cache(guild_id, message.clone()).await; let page: Arc> = Arc::new(RwLock::new(0)); diff --git a/crack-core/src/commands/music/remove.rs b/crack-core/src/commands/music/remove.rs index 8d1241962..ffa599897 100644 --- a/crack-core/src/commands/music/remove.rs +++ b/crack-core/src/commands/music/remove.rs @@ -4,7 +4,7 @@ use crate::{ handlers::track_end::update_queue_messages, messaging::message::CrackedMessage, messaging::messages::REMOVED_QUEUE, - utils::send_response_poise_text, + utils::send_reply, utils::{get_track_metadata, send_embed_response_poise}, Context, Error, }; @@ -14,15 +14,33 @@ use std::cmp::min; /// Remove track(s) from the queue. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command(category = "Music", prefix_command, slash_command, guild_only)] pub async fn remove( ctx: Context<'_>, - #[description = "Start index in the track queue to remove"] b_index: usize, + #[description = "Index in the queue to remove (Or number of tracks to remove if no second argument."] + b_index: usize, #[description = "End index in the track queue to remove"] e_index: Option, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); + if help { + return crate::commands::help::wrapper(ctx).await; + } + remove_internal(ctx, b_index, e_index).await +} + +/// Internal remove function. +pub async fn remove_internal( + ctx: Context<'_>, + b_index: usize, + e_index: Option, +) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(ctx.serenity_context()) + .await + .ok_or(CrackedError::NoSongbird)?; + let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; let remove_index = b_index; let remove_until = match e_index { @@ -64,9 +82,9 @@ pub async fn remove( if remove_until == remove_index { let embed = create_remove_enqueued_embed(track).await; //send_embed_response(&ctx.serenity_context().http, interaction, embed).await?; - send_embed_response_poise(ctx, embed).await?; + send_embed_response_poise(&ctx, embed).await?; } else { - send_response_poise_text(ctx, CrackedMessage::RemoveMultiple).await?; + send_reply(&ctx, CrackedMessage::RemoveMultiple, true).await?; } update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; diff --git a/crack-core/src/commands/music/repeat.rs b/crack-core/src/commands/music/repeat.rs index 4d9c0061e..d23dc6700 100644 --- a/crack-core/src/commands/music/repeat.rs +++ b/crack-core/src/commands/music/repeat.rs @@ -1,34 +1,57 @@ use crate::{ - errors::CrackedError, messaging::message::CrackedMessage, messaging::messages::FAIL_LOOP, - utils::send_response_poise, Context, Error, + commands::cmd_check_music, errors::CrackedError, messaging::message::CrackedMessage, + messaging::messages::FAIL_LOOP, utils::send_reply, Context, Error, }; use songbird::tracks::{LoopState, TrackHandle}; /// Toggle looping of the current track. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] -pub async fn repeat(ctx: Context<'_>) -> Result<(), Error> { +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only +)] +pub async fn repeat( + ctx: Context<'_>, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } + repeat_internal(ctx).await +} + +/// Internal repeat function. +#[cfg(not(tarpaulin_include))] +pub async fn repeat_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let manager = songbird::get(ctx.serenity_context()) .await .ok_or(CrackedError::NoSongbird)?; - let call = manager.get(guild_id).ok_or(CrackedError::NoSongbird)?; + let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; let handler = call.lock().await; - let track = handler.queue().current().unwrap(); + let track = match handler.queue().current() { + Some(track) => track, + None => return Err(Box::new(CrackedError::NothingPlaying)), + }; + drop(handler); - let was_looping = track.get_info().await.unwrap().loops == LoopState::Infinite; + let was_looping = track.get_info().await?.loops == LoopState::Infinite; let toggler = if was_looping { TrackHandle::disable_loop } else { TrackHandle::enable_loop }; - let msg = match toggler(&track) { - Ok(_) if was_looping => send_response_poise(ctx, CrackedMessage::LoopDisable, true).await, - Ok(_) if !was_looping => send_response_poise(ctx, CrackedMessage::LoopEnable, true).await, + let _ = match toggler(&track) { + Ok(_) if was_looping => send_reply(&ctx, CrackedMessage::LoopDisable, true).await, + Ok(_) if !was_looping => send_reply(&ctx, CrackedMessage::LoopEnable, true).await, _ => Err(CrackedError::Other(FAIL_LOOP)), }?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/music/resume.rs b/crack-core/src/commands/music/resume.rs index 118e9a4d7..c65b02494 100644 --- a/crack-core/src/commands/music/resume.rs +++ b/crack-core/src/commands/music/resume.rs @@ -1,20 +1,19 @@ use crate::{ errors::{verify, CrackedError}, messaging::message::CrackedMessage, - utils::send_response_poise_text, + utils::send_reply, {Context, Error}, }; /// Resume the current track. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] -pub async fn resume( - ctx: Context<'_>, - #[description = "Resume the music."] _send_reply: Option, -) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); +#[poise::command(category = "Music", slash_command, prefix_command, guild_only)] +pub async fn resume(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(ctx.serenity_context()) + .await + .ok_or(CrackedError::NoSongbird)?; + let call = manager.get(guild_id).ok_or(CrackedError::NotConnected)?; let handler = call.lock().await; let queue = handler.queue(); @@ -22,9 +21,7 @@ pub async fn resume( verify(!queue.is_empty(), CrackedError::NothingPlaying)?; verify(queue.resume(), CrackedError::Other("Failed resuming track"))?; - // FIXME: Do we want to do the send_reply parameter? - let msg = send_response_poise_text(ctx, CrackedMessage::Resume).await?; + send_reply(&ctx, CrackedMessage::Resume, false).await?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/music/seek.rs b/crack-core/src/commands/music/seek.rs index b92d85afc..2869770c0 100644 --- a/crack-core/src/commands/music/seek.rs +++ b/crack-core/src/commands/music/seek.rs @@ -2,22 +2,20 @@ use crate::{ errors::{verify, CrackedError}, messaging::message::CrackedMessage, messaging::messages::{FAIL_MINUTES_PARSING, FAIL_SECONDS_PARSING}, - utils::send_response_poise, + poise_ext::ContextExt, + utils::send_reply, Context, Error, }; use std::time::Duration; /// Seek to timestamp, in format `mm:ss`. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command(category = "Music", prefix_command, slash_command, guild_only)] pub async fn seek( ctx: Context<'_>, #[description = "Seek to timestamp, in format `mm:ss`."] seek_time: String, ) -> Result<(), Error> { - // let mut interaction = get_interaction(ctx).unwrap(); - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); + let call = ctx.get_call().await?; let timestamp_str = seek_time.as_str(); let mut units_iter = timestamp_str.split(':'); @@ -39,14 +37,13 @@ pub async fn seek( let _callback = track.seek(Duration::from_secs(timestamp)); - let msg = send_response_poise( - ctx, + let _ = send_reply( + &ctx, CrackedMessage::Seek { timestamp: timestamp_str.to_owned(), }, true, ) .await?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/music/shuffle.rs b/crack-core/src/commands/music/shuffle.rs index 1d50f8779..6dfd51ffd 100644 --- a/crack-core/src/commands/music/shuffle.rs +++ b/crack-core/src/commands/music/shuffle.rs @@ -1,16 +1,22 @@ use crate::{ - handlers::track_end::update_queue_messages, messaging::message::CrackedMessage, - utils::send_response_poise, Context, Error, + commands::cmd_check_music, handlers::track_end::update_queue_messages, + messaging::message::CrackedMessage, poise_ext::ContextExt, utils::send_reply, Context, + CrackedError, Error, }; use rand::Rng; /// Shuffle the current queue. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only +)] pub async fn shuffle(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let call = ctx.get_call().await?; let handler = call.lock().await; handler.queue().modify_queue(|queue| { @@ -25,7 +31,7 @@ pub async fn shuffle(ctx: Context<'_>) -> Result<(), Error> { let queue = handler.queue().current_queue(); drop(handler); - send_response_poise(ctx, CrackedMessage::Shuffle, true).await?; + send_reply(&ctx, CrackedMessage::Shuffle, true).await?; update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; Ok(()) } diff --git a/crack-core/src/commands/music/skip.rs b/crack-core/src/commands/music/skip.rs index cf0b98eeb..8470c08fb 100644 --- a/crack-core/src/commands/music/skip.rs +++ b/crack-core/src/commands/music/skip.rs @@ -1,7 +1,11 @@ +use crate::poise_ext::ContextExt; use crate::{ + commands::cmd_check_music, + commands::get_call_or_join_author, errors::{verify, CrackedError}, messaging::message::CrackedMessage, - utils::{get_track_metadata, send_response_poise_text}, + poise_ext::PoiseContextExt, + utils::get_track_metadata, Context, Error, }; use serenity::all::Message; @@ -11,27 +15,19 @@ use tokio::sync::MutexGuard; /// Skip the current track, or a number of tracks. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only +)] pub async fn skip( ctx: Context<'_>, - #[description = "Number of tracks to skip"] tracks_to_skip: Option, + #[description = "Number of tracks to skip"] num_tracks: Option, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; - let manager = songbird::get(ctx.serenity_context()) - .await - .ok_or(CrackedError::NoSongbird)?; - let call = match manager.get(guild_id) { - Some(call) => call, - None => { - tracing::warn!( - "Not in voice channel: manager.get({}) returned None", - guild_id - ); - return Ok(()); - }, - }; - - let to_skip = tracks_to_skip.unwrap_or(1); + let (call, guild_id) = ctx.get_call_guild_id().await?; + let to_skip = num_tracks.unwrap_or(1); let handler = call.lock().await; let queue = handler.queue(); @@ -46,7 +42,7 @@ pub async fn skip( force_skip_top_track(&handler).await?; let msg = create_skip_response(ctx, &handler, tracks_to_skip).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + ctx.data().add_msg_to_cache(guild_id, msg).await; Ok(()) } @@ -58,50 +54,53 @@ pub async fn create_skip_response( handler: &MutexGuard<'_, Call>, tracks_to_skip: usize, ) -> Result { - match handler.queue().current() { + let send_msg = match handler.queue().current() { Some(track) => { let metadata = get_track_metadata(&track).await; - send_response_poise_text( - ctx, - CrackedMessage::SkipTo { - title: metadata.title.as_ref().unwrap().to_owned(), - url: metadata.source_url.as_ref().unwrap().to_owned(), - }, - ) - .await + CrackedMessage::SkipTo { + title: metadata.title.as_ref().unwrap().to_owned(), + url: metadata.source_url.as_ref().unwrap().to_owned(), + } }, None => { if tracks_to_skip > 1 { - send_response_poise_text(ctx, CrackedMessage::SkipAll).await + CrackedMessage::SkipAll } else { - send_response_poise_text(ctx, CrackedMessage::Skip).await + CrackedMessage::Skip } }, - } + }; + ctx.send_reply(send_msg, true) + .await? + .into_message() + .await + .map_err(|e| e.into()) } /// Downvote and skip song causing it to *not* be used in music recommendations. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Music", + check = "cmd_check_music", + prefix_command, + slash_command, + guild_only +)] pub async fn downvote(ctx: Context<'_>) -> Result<(), Error> { - use crate::commands::get_call_with_fail_msg; - let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let call = get_call_with_fail_msg(ctx).await?; + let call = get_call_or_join_author(ctx).await?; let handler = call.lock().await; let queue = handler.queue(); let metadata = get_track_metadata(&queue.current().unwrap()).await; let source_url = &metadata.source_url.ok_or("ASDF").unwrap(); - let res1 = ctx.data().downvote_track(guild_id, source_url); - - let res2 = force_skip_top_track(&handler); - - tracing::warn!("downvoting track: {}", source_url); + let res1 = ctx.data().downvote_track(guild_id, source_url).await?; + let res2 = force_skip_top_track(&handler).await?; - tokio::join!(res1, res2).0?; + tracing::warn!("downvoted track: {:#?}", res1); + tracing::warn!("refetched queue: {:#?}", res2); Ok(()) } diff --git a/crack-core/src/commands/music/stop.rs b/crack-core/src/commands/music/stop.rs index 02bfeae54..706b74023 100644 --- a/crack-core/src/commands/music/stop.rs +++ b/crack-core/src/commands/music/stop.rs @@ -1,24 +1,37 @@ +use songbird::tracks::TrackHandle; + use crate::{ + commands::cmd_check_music, errors::{verify, CrackedError}, guild::operations::GuildSettingsOperations, - handlers::track_end::update_queue_messages, messaging::message::CrackedMessage, - utils::send_response_poise_text, + poise_ext::ContextExt, + utils::send_reply, Context, Error, }; -/// Stop the current track. +/// Stop the current track and clear the queue. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + guild_only +)] pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + stop_internal(ctx).await?; + Ok(()) +} + +/// The return vector from this should be empty. +#[cfg(not(tarpaulin_include))] +pub async fn stop_internal(ctx: Context<'_>) -> Result, Error> { + let (call, guild_id) = ctx.get_call_guild_id().await?; ctx.data().set_autoplay(guild_id, false).await; - let manager = songbird::get(ctx.serenity_context()).await.unwrap(); - let call = manager.get(guild_id).unwrap(); let handler = call.lock().await; let queue = handler.queue(); - // Do we want to return an error here or just pritn and return/? verify(!queue.is_empty(), CrackedError::NothingPlaying)?; queue.stop(); @@ -27,8 +40,6 @@ pub async fn stop(ctx: Context<'_>) -> Result<(), Error> { let queue = handler.queue().current_queue(); drop(handler); - update_queue_messages(&ctx.serenity_context().http, ctx.data(), &queue, guild_id).await; - let msg = send_response_poise_text(ctx, CrackedMessage::Stop).await?; - ctx.data().add_msg_to_cache(guild_id, msg); - Ok(()) + send_reply(&ctx, CrackedMessage::Stop, true).await?; + Ok(queue) } diff --git a/crack-core/src/commands/music/summon.rs b/crack-core/src/commands/music/summon.rs index 80923032d..d5b7404c3 100644 --- a/crack-core/src/commands/music/summon.rs +++ b/crack-core/src/commands/music/summon.rs @@ -1,20 +1,17 @@ -use self::serenity::{model::id::ChannelId, Mentionable}; -use crate::handlers::IdleHandler; +use crate::commands::{cmd_check_music, do_join, help, sub_help as help}; use crate::{ - connection::get_voice_channel_for_user, errors::CrackedError, handlers::TrackEndHandler, - messaging::message::CrackedMessage, utils::get_user_id, Context, Error, + connection::get_voice_channel_for_user_summon, errors::CrackedError, poise_ext::ContextExt, + Context, Error, }; -use ::serenity::all::{Channel, Guild, UserId}; -use poise::{serenity_prelude as serenity, CreateReply}; +use ::serenity::all::{Channel, ChannelId, Mentionable}; use songbird::Call; -use songbird::{Event, TrackEvent}; use std::sync::Arc; -use std::time::Duration; use tokio::sync::Mutex; -/// Summon the bot to a voice channel. -#[cfg(not(tarpaulin_include))] +/// Summon the bot to your voice channel. #[poise::command( + category = "Music", + check = "cmd_check_music", slash_command, prefix_command, aliases("join", "come here", "comehere", "come", "here"), @@ -22,24 +19,52 @@ use tokio::sync::Mutex; )] pub async fn summon( ctx: Context<'_>, - #[description = "Channel to join"] channel: Option, - #[description = "Channel id to join"] channel_id_str: Option, + #[flag] + #[description = "Show a help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + summon_internal(ctx, None, None).await +} + +/// Summon a bot to a specific voice channel. +#[poise::command( + category = "Music", + slash_command, + prefix_command, + check = "cmd_check_music", + subcommands("help"), + guild_only +)] +pub async fn summonchannel( + ctx: Context<'_>, + #[description = "Channel to summon the bot to."] channel: Option, + #[description = "Channel Id of the channel to summon the bot to."] channel_id_str: Option< + String, + >, +) -> Result<(), Error> { + summon_internal(ctx, channel, channel_id_str).await +} + +/// Internal method to handle summonging the bot to a voice channel. +pub async fn summon_internal( + ctx: Context<'_>, + channel: Option, + channel_id_str: Option, ) -> Result<(), Error> { let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; - let guild = ctx - .serenity_context() - .cache - .guild(guild_id) - .unwrap() - .clone(); let manager = songbird::get(ctx.serenity_context()).await.unwrap(); + let guild = ctx.guild().ok_or(CrackedError::NoGuildCached)?.clone(); + let user_id = ctx.get_user_id(); - let user_id = get_user_id(&ctx); - - let channel_id = - get_channel_id_for_summon(channel, channel_id_str, guild.clone(), user_id).await?; + let channel_id = match parse_channel_id(channel, channel_id_str)? { + Some(id) => id, + None => get_voice_channel_for_user_summon(&guild, &user_id)?, + }; - let call: Arc> = match manager.get(guild_id) { + let _call: Arc> = match manager.get(guild_id) { Some(call) => { let handler = call.lock().await; let has_current_connection = handler.current_connection().is_some(); @@ -52,137 +77,55 @@ pub async fn summon( Ok(call.clone()) } }, - None => manager.join(guild_id, channel_id).await.map_err(|e| { - tracing::error!("Error joining channel: {:?}", e); - CrackedError::JoinChannelError(e) - }), + None => do_join(ctx, &manager, guild_id, channel_id) + .await + .map_err(Into::into), }?; - // // join the channel - // let result = manager.join(guild.id, channel_id).await?; - let buffer = { - // // Open the data lock in write mode, so keys can be inserted to it. - // let mut data = ctx.data().write().await; - - // // So, we have to insert the same type to it. - // data.insert::>(Arc::new(RwLock::new(Vec::new()))); - let data = Arc::new(tokio::sync::RwLock::new(Vec::new())); - data.clone() - }; - - use std::sync::atomic::AtomicBool; - - use crate::handlers::voice::register_voice_handlers; - - // FIXME - // use crate::handlers::voice::register_voice_handlers; - - { - let mut handler = call.lock().await; - // unregister existing events and register idle notifier - handler.remove_all_global_events(); - } - { - let _ = register_voice_handlers(buffer, call.clone(), ctx.serenity_context().clone()).await; - let mut handler = call.lock().await; - { - let guild_settings_map = ctx.data().guild_settings_map.write().await; - - // guild_settings_map - // .entry(guild_id) - // .and_modify(|guild_settings| { - // // // guild_settings.channel_id = Some(channel_id); - // // if guild_settings.volume <= 0.1 { - // // guild_settings.volume = 0.7; - // // guild_settings.old_volume = 0.7; - // // } - // }); - let _ = guild_settings_map.get(&guild_id).map(|guild_settings| { - let timeout = guild_settings.timeout; - if timeout > 0 { - let premium = guild_settings.premium; - - handler.add_global_event( - Event::Periodic(Duration::from_secs(5), None), - IdleHandler { - http: ctx.serenity_context().http.clone(), - manager: manager.clone(), - channel_id, - guild_id: Some(guild_id), - limit: timeout as usize, - count: Default::default(), - no_timeout: Arc::new(AtomicBool::new(premium)), - }, - ); - } - }); - } - - handler.add_global_event( - Event::Track(TrackEvent::End), - TrackEndHandler { - guild_id: guild.id, - http: ctx.serenity_context().http.clone(), - cache: ctx.serenity_context().cache.clone(), - call: call.clone(), - data: ctx.data().clone(), - }, - ); - - let text = CrackedMessage::Summon { - mention: channel_id.mention(), - } - .to_string(); - let msg = ctx - .send(CreateReply::default().content(text).ephemeral(true)) - .await? - .into_message() - .await?; - ctx.data().add_msg_to_cache(guild_id, msg); - } + // set_global_handlers(ctx, call, guild_id, channel_id).await?; Ok(()) } -async fn get_channel_id_for_summon( +/// Internal method to parse the channel id. +fn parse_channel_id( channel: Option, channel_id_str: Option, - guild: Guild, - user_id: UserId, -) -> Result { +) -> Result, Error> { if let Some(channel) = channel { - return Ok(channel.id()); + return Ok(Some(channel.id())); } match channel_id_str { Some(id) => { tracing::warn!("channel_id_str: {:?}", id); match id.parse::() { - Ok(id) => Ok(ChannelId::new(id)), - Err(_) => match get_voice_channel_for_user(&guild, &user_id) { - Ok(channel_id) => Ok(channel_id), - Err(_) => get_voice_channel_for_user_with_error(&guild, &user_id), - }, + Ok(id) => Ok(Some(ChannelId::new(id))), + Err(e) => Err(e.into()), } }, - None => get_voice_channel_for_user_with_error(&guild, &user_id), + None => Ok(None), } } -fn get_voice_channel_for_user_with_error( - guild: &Guild, - user_id: &UserId, -) -> Result { - match get_voice_channel_for_user(guild, user_id) { - Ok(channel_id) => Ok(channel_id), - Err(_) => { - // ctx.say("You are not in a voice channel!").await?; - tracing::warn!( - "User {} is not in a voice channel in guild {}", - user_id, - guild.id - ); - Err(CrackedError::WrongVoiceChannel.into()) - }, +#[cfg(test)] +mod test { + use crate::commands::music::summon::parse_channel_id; + use serenity::model::id::ChannelId; + + #[test] + fn test_parse_channel_id() { + let channel = None; + + assert_eq!(parse_channel_id(channel, None).unwrap(), None); + assert_eq!( + parse_channel_id(None, Some("123".to_string())).unwrap(), + Some(ChannelId::new(123)) + ); + assert_eq!( + parse_channel_id(None, Some("abc".to_string())).is_err(), + true + ); + assert_eq!(parse_channel_id(None, None).unwrap(), None); } } diff --git a/crack-core/src/commands/music/volume.rs b/crack-core/src/commands/music/volume.rs index 364013a7c..d420ead1a 100644 --- a/crack-core/src/commands/music/volume.rs +++ b/crack-core/src/commands/music/volume.rs @@ -1,4 +1,5 @@ use self::serenity::builder::CreateEmbed; +use crate::commands::{cmd_check_music, help, ContextExt}; use crate::errors::CrackedError; use crate::guild::settings::GuildSettings; use crate::utils::{get_guild_name, send_embed_response_poise}; @@ -9,11 +10,35 @@ use songbird::tracks::TrackHandle; /// Get or set the volume of the bot. #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only, aliases("vol"))] +#[poise::command( + category = "Music", + check = "cmd_check_music", + aliases("vol"), + slash_command, + prefix_command, + guild_only +)] pub async fn volume( ctx: Context<'_>, #[description = "Set the volume of the bot"] level: Option, + #[flag] + #[description = "Show a help menu for this command."] + help: bool, ) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + volume_internal(&ctx, level).await +} + +#[cfg(not(tarpaulin_include))] +/// Internal method to handle volume changes. +pub async fn volume_internal<'ctx>( + ctx: &'ctx Context<'_>, + level: Option, +) -> Result<(), Error> { + use crate::guild::operations::GuildSettingsOperations; + tracing::error!("volume"); let prefix = ctx.data().bot_settings.get_prefix(); let guild_id = match ctx.guild_id() { @@ -35,8 +60,7 @@ pub async fn volume( tracing::error!("Can't get manager."); let embed = CreateEmbed::default().description(format!("{}", CrackedError::NotConnected)); - let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_embed_response_poise(ctx, embed).await?; return Ok(()); }, }; @@ -46,8 +70,7 @@ pub async fn volume( tracing::error!("Can't get call from manager."); let embed = CreateEmbed::default().description(format!("{}", CrackedError::NotConnected)); - let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_embed_response_poise(ctx, embed).await?; return Ok(()); }, }; @@ -63,26 +86,17 @@ pub async fn volume( Some(handle) => handle.get_info().await.unwrap().volume, None => 0.1, }; + let prefix = Some(&prefix).map(|s| s.as_str()); + let name = get_guild_name(ctx.serenity_context(), guild_id).await; ctx.data() - .guild_settings_map - .write() - .await - .entry(guild_id) - .or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(&prefix), - get_guild_name(ctx.serenity_context(), guild_id), - ) - }); + .get_or_create_guild_settings(guild_id, name, prefix) + .await; + let guild_settings = ctx - .data() - .guild_settings_map - .read() + .get_guild_settings(guild_id) .await - .get(&guild_id) - .unwrap() - .clone(); + .ok_or(CrackedError::NoGuildSettings)?; + let asdf = guild_settings.volume; tracing::warn!( @@ -95,12 +109,12 @@ pub async fn volume( guild_settings.volume * 100.0, volume_track * 100.0 )); - let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_embed_response_poise(ctx, embed).await?; return Ok(()); }, }; + let name = get_guild_name(ctx.serenity_context(), guild_id).await; let new_vol = (to_set.unwrap() as f32) / 100.0; let old_vol = { let mut guild_settings_guard = ctx.data().guild_settings_map.write().await; @@ -110,13 +124,9 @@ pub async fn volume( guild_settings.set_volume(new_vol); }) .or_insert_with(|| { - let guild_settings = GuildSettings::new( - guild_id, - Some(&prefix), - get_guild_name(ctx.serenity_context(), guild_id), - ) - .set_volume(new_vol) - .clone(); + let guild_settings = GuildSettings::new(guild_id, Some(&prefix), name) + .set_volume(new_vol) + .clone(); tracing::warn!( "guild_settings: {:?}", @@ -136,24 +146,16 @@ pub async fn volume( let track_handle: TrackHandle = match track_handle { Some(handle) => handle, None => { - return send_embed_response_poise(ctx, embed) - .await - .map(|m| { - ctx.data().add_msg_to_cache(guild_id, m); - }) - .map_err(Into::into); + let _ = send_embed_response_poise(ctx, embed).await?; + return Ok(()); }, }; track_handle.set_volume(new_vol).unwrap(); embed }; - send_embed_response_poise(ctx, embed) - .await - .map(|m| { - ctx.data().add_msg_to_cache(guild_id, m); - }) - .map_err(Into::into) + let _ = send_embed_response_poise(ctx, embed).await?; + Ok(()) } pub fn create_volume_embed(old: f32, new: f32) -> CreateEmbed { diff --git a/crack-core/src/commands/music/vote.rs b/crack-core/src/commands/music/vote.rs index 97b13f7ea..8a29730c9 100644 --- a/crack-core/src/commands/music/vote.rs +++ b/crack-core/src/commands/music/vote.rs @@ -1,6 +1,7 @@ use crate::db; use crate::errors::CrackedError; use crate::http_utils; +use crate::poise_ext::ContextExt; use crate::{ messaging::messages::{ VOTE_TOPGG_LINK_TEXT, VOTE_TOPGG_NOT_VOTED, VOTE_TOPGG_TEXT, VOTE_TOPGG_URL, @@ -18,36 +19,22 @@ pub struct CheckResponse { /// Vote link for cracktunes on top.gg #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command)] +#[poise::command(category = "Base", slash_command, prefix_command)] pub async fn vote(ctx: Context<'_>) -> Result<(), Error> { - vote_internal(ctx).await + vote_topgg_internal(ctx).await } /// Internal vote function without the #command macro #[cfg(not(tarpaulin_include))] -pub async fn vote_internal(ctx: Context<'_>) -> Result<(), Error> { +pub async fn vote_topgg_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id: Option = ctx.guild_id(); - let user_id: UserId = ctx.author().id; + let bot_id: UserId = http_utils::get_bot_id(ctx).await?; tracing::info!("user_id: {:?}, guild_id: {:?}", user_id, guild_id); - let bot_id: UserId = http_utils::get_bot_id(ctx).await?; tracing::info!("bot_id: {:?}", bot_id); - let has_voted = match check_and_record_vote( - ctx.data().database_pool.as_ref().unwrap(), - user_id.get() as i64, - ctx.author().name.clone(), - bot_id.get() as i64, - ) - .await - { - Ok(v) => v, - Err(e) => { - tracing::error!("Error checking and recording vote: {:?}", e); - false - }, - }; + let has_voted = ctx.check_and_record_vote().await?; let msg_str = if has_voted { VOTE_TOPGG_VOTED diff --git a/crack-core/src/commands/music/voteskip.rs b/crack-core/src/commands/music/voteskip.rs index 19ebd13c3..dc21f481e 100644 --- a/crack-core/src/commands/music/voteskip.rs +++ b/crack-core/src/commands/music/voteskip.rs @@ -1,20 +1,44 @@ -use self::serenity::{model::id::GuildId, Mentionable}; use crate::{ - commands::music::{create_skip_response, force_skip_top_track}, + commands::{ + cmd_check_music, + music::{create_skip_response, force_skip_top_track}, + }, connection::get_voice_channel_for_user, errors::{verify, CrackedError}, guild::cache::GuildCacheMap, messaging::message::CrackedMessage, - utils::{get_user_id, send_response_poise_text}, + poise_ext::ContextExt, + utils::send_reply_embed, Context, Data, Error, }; use poise::serenity_prelude as serenity; +use serenity::{model::id::GuildId, Mentionable}; use std::collections::HashSet; /// Vote to skip the current track #[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command, guild_only)] -pub async fn voteskip(ctx: Context<'_>) -> Result<(), Error> { +#[poise::command( + category = "Music", + check = "cmd_check_music", + slash_command, + prefix_command, + guild_only +)] +pub async fn voteskip( + ctx: Context<'_>, + #[flag] + #[description = "Show the help menu."] + help: bool, +) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } + voteskip_internal(ctx).await +} + +/// Internal function for voteskip +#[cfg(not(tarpaulin_include))] +async fn voteskip_internal(ctx: Context<'_>) -> Result<(), Error> { // use crate::db::TrackReaction; let guild_id = ctx.guild_id().unwrap(); @@ -39,7 +63,7 @@ pub async fn voteskip(ctx: Context<'_>) -> Result<(), Error> { let cache_map = data.get_mut::().unwrap(); let cache = cache_map.entry(guild_id).or_default(); - let user_id = get_user_id(&ctx); + let user_id = ctx.get_user_id(); cache.current_skip_votes.insert(user_id); let guild_users = ctx @@ -54,7 +78,7 @@ pub async fn voteskip(ctx: Context<'_>) -> Result<(), Error> { .filter(|v| v.channel_id.unwrap() == bot_channel_id); let skip_threshold = channel_guild_users.count() / 2; - let msg = if cache.current_skip_votes.len() >= skip_threshold { + let _ = if cache.current_skip_votes.len() >= skip_threshold { // // Write the skip votes to the db // TrackReaction::insert( // &ctx.data().database_pool, @@ -68,16 +92,18 @@ pub async fn voteskip(ctx: Context<'_>) -> Result<(), Error> { force_skip_top_track(&handler).await?; create_skip_response(ctx, &handler, 1).await } else { - send_response_poise_text( - ctx, + send_reply_embed( + &ctx, CrackedMessage::VoteSkip { - mention: get_user_id(&ctx).mention(), + mention: ctx.get_user_id().mention(), missing: skip_threshold - cache.current_skip_votes.len(), }, ) + .await? + .into_message() .await + .map_err(|e| e.into()) }?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/music_utils.rs b/crack-core/src/commands/music_utils.rs index dcac5438a..26feb1908 100644 --- a/crack-core/src/commands/music_utils.rs +++ b/crack-core/src/commands/music_utils.rs @@ -2,13 +2,11 @@ use crate::connection::get_voice_channel_for_user; use crate::guild::operations::GuildSettingsOperations; use crate::handlers::{IdleHandler, TrackEndHandler}; use crate::messaging::message::CrackedMessage; -use crate::utils::send_embed_response_poise; -use crate::ContextExt as _; +use crate::utils::send_reply_embed; use crate::CrackedError; use crate::{Context, Error}; use poise::serenity_prelude::Mentionable; -use poise::CreateReply; -use serenity::all::{ChannelId, CreateEmbed, GuildId}; +use serenity::all::{ChannelId, GuildId}; use songbird::{Call, Event, TrackEvent}; use std::{ sync::{atomic::AtomicBool, Arc}, @@ -23,16 +21,28 @@ pub async fn set_global_handlers( call: Arc>, guild_id: GuildId, channel_id: ChannelId, -) -> Result { - let data = ctx.data(); +) -> Result<(), CrackedError> { + use crate::handlers::voice::register_voice_handlers; + let data = ctx.data(); let manager = songbird::get(ctx.serenity_context()) .await .ok_or(CrackedError::NoSongbird)?; - let mut handler = call.lock().await; + // This is the temp buffer to hold voice data for processing + let buffer = { + // // Open the data lock in write mode, so keys can be inserted to it. + // let mut data = ctx.data().write().await; + // data.insert::>(Arc::new(RwLock::new(Vec::new()))); + let data = Arc::new(tokio::sync::RwLock::new(Vec::new())); + data.clone() + }; + // unregister existing events and register idle notifier - handler.remove_all_global_events(); + call.lock().await.remove_all_global_events(); + register_voice_handlers(buffer, call.clone(), ctx.serenity_context().clone()).await?; + + let mut handler = call.lock().await; let guild_settings = data .get_guild_settings(guild_id) @@ -62,58 +72,66 @@ pub async fn set_global_handlers( guild_id, cache: ctx.serenity_context().cache.clone(), http: ctx.serenity_context().http.clone(), - // cache_http: Arc::new(ctx.serenity_context()), call: call.clone(), data: ctx.data().clone(), }, ); - let text = CrackedMessage::Summon { - mention: channel_id.mention(), - } - .to_string(); - - Ok(text) + Ok(()) } /// Get the call handle for songbird. #[cfg(not(tarpaulin_include))] -pub async fn get_call_with_fail_msg(ctx: Context<'_>) -> Result>, CrackedError> { +pub async fn get_call_or_join_author(ctx: Context<'_>) -> Result>, CrackedError> { let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let manager = songbird::get(ctx.serenity_context()) .await .ok_or(CrackedError::NoSongbird)?; // Return the call if it already exists - let maybe_call = manager.get(guild_id); - if let Some(call) = maybe_call { + if let Some(call) = manager.get(guild_id) { return Ok(call); } - // Otherwise, try to join the channel of the user who sent the message. let channel_id = { let guild = ctx.guild().ok_or(CrackedError::NoGuildCached)?; get_voice_channel_for_user(&guild.clone(), &ctx.author().id)? }; - match manager.join(guild_id, channel_id).await { + + let call: Arc> = do_join(ctx, &manager, guild_id, channel_id).await?; + + Ok(call) +} + +/// Join a voice channel. +pub async fn do_join( + ctx: Context<'_>, + manager: &songbird::Songbird, + guild_id: GuildId, + channel_id: ChannelId, +) -> Result>, Error> { + let call = manager.join(guild_id, channel_id); + let call = tokio::time::timeout(Duration::from_secs(5), call).await?; + match call { // If we successfully joined the channel, set the global handlers. // TODO: This should probably be a separate function. Ok(call) => { - let text = set_global_handlers(ctx, call.clone(), guild_id, channel_id).await?; - - let msg = ctx - .send(CreateReply::default().content(text).ephemeral(true)) - .await? - .into_message() - .await?; - ctx.add_msg_to_cache_nonasync(guild_id, msg); + set_global_handlers(ctx, call.clone(), guild_id, channel_id).await?; + let msg = CrackedMessage::Summon { + mention: channel_id.mention(), + }; + send_reply_embed(&ctx, msg).await?; Ok(call) }, Err(err) => { // FIXME: Do something smarter here also. - let embed = CreateEmbed::default().description(format!("{}", err)); - send_embed_response_poise(ctx, embed).await?; - Err(CrackedError::JoinChannelError(err)) + //let embed = CreateEmbed::default().description(format!("{}", err)); + //send_embed_response_poise(&ctx, embed).await?; + let str = err.to_string().clone(); + let my_err = CrackedError::JoinChannelError(err); + let message = CrackedMessage::CrackedRed(str.clone()); + send_reply_embed(&ctx, message).await?; + Err(Box::new(my_err)) }, } } diff --git a/crack-core/src/commands/osint.rs b/crack-core/src/commands/osint.rs index 74c0287c8..99f5c1f2c 100644 --- a/crack-core/src/commands/osint.rs +++ b/crack-core/src/commands/osint.rs @@ -1,17 +1,18 @@ -use crate::utils::send_channel_message; -pub use crate::{ +use crate::{ + commands::sub_help as help, + http_utils::{CacheHttpExt, SendMessageParams}, messaging::message::CrackedMessage, - utils::{send_response_poise, SendMessageParams}, + utils::send_reply_embed, Context, Error, }; -use crack_osint::VirusTotalClient; +use crack_osint::{check_password_pwned, VirusTotalClient}; use crack_osint::{get_scan_result, scan_url}; use poise::CreateReply; use std::result::Result; -use std::sync::Arc; /// Osint Commands #[poise::command( + category = "OsInt", prefix_command, slash_command, subcommands( @@ -21,11 +22,12 @@ use std::sync::Arc; // "socialmedia", // "wayback", // "whois", - // "checkpass", // "phlookup", // "phcode", + "checkpass", "scan", "virustotal_result", + "help", ), )] pub async fn osint(ctx: Context<'_>) -> Result<(), Error> { @@ -40,7 +42,9 @@ pub async fn osint(ctx: Context<'_>) -> Result<(), Error> { .await? .into_message() .await?; - ctx.data().add_msg_to_cache(ctx.guild_id().unwrap(), msg); + ctx.data() + .add_msg_to_cache(ctx.guild_id().unwrap(), msg) + .await; tracing::warn!("{}", msg_str.clone()); Ok(()) @@ -84,23 +88,22 @@ pub async fn scan(ctx: Context<'_>, url: String) -> Result<(), Error> { ephemeral: false, reply: true, msg: message, + ..Default::default() }; - let _msg = send_channel_message(Arc::new(ctx.http()), params).await?; + let _msg = ctx.send_channel_message(params).await?; Ok(()) } #[cfg(not(tarpaulin_include))] #[poise::command(prefix_command, slash_command)] pub async fn virustotal_result(ctx: Context<'_>, id: String) -> Result<(), Error> { - use crate::http_utils; - ctx.reply("Scanning...").await?; let api_key = std::env::var("VIRUSTOTAL_API_KEY") .map_err(|_| crate::CrackedError::Other("VIRUSTOTAL_API_KEY"))?; let channel_id = ctx.channel_id(); tracing::info!("channel_id: {}", channel_id); - let client = VirusTotalClient::new(&api_key, http_utils::get_client().clone()); + let client = VirusTotalClient::new(&api_key, crate::http_utils::get_client().clone()); tracing::info!("client: {:?}", client); @@ -120,9 +123,25 @@ pub async fn virustotal_result(ctx: Context<'_>, id: String) -> Result<(), Error ephemeral: false, reply: true, msg: message, + ..Default::default() + }; + + let _msg = ctx.send_channel_message(params).await?; + Ok(()) +} + +/// Check if a password has been pwned. +#[poise::command(prefix_command, hide_in_help)] +pub async fn checkpass(ctx: Context<'_>, password: String) -> Result<(), Error> { + let pwned = check_password_pwned(&password).await?; + let message = if pwned { + CrackedMessage::PasswordPwned + } else { + CrackedMessage::PasswordSafe }; - let _msg = send_channel_message(Arc::new(ctx.http()), params).await?; + send_reply_embed(&ctx, message).await?; + Ok(()) } diff --git a/crack-core/src/commands/permissions.rs b/crack-core/src/commands/permissions.rs new file mode 100644 index 000000000..7914c33bf --- /dev/null +++ b/crack-core/src/commands/permissions.rs @@ -0,0 +1,74 @@ +use crate::{guild::operations::GuildSettingsOperations, Context, CrackedError, Error}; +use poise::serenity_prelude as serenity; +use serenity::all::{ChannelId, Member, Permissions, RoleId}; +use std::borrow::Cow; + +/// Public function to check if the user is authorized to use the music commands. +pub async fn cmd_check_music(ctx: Context<'_>) -> Result { + if ctx.author().bot { + return Ok(false); + }; + + let channel_id: ChannelId = ctx.channel_id(); + let member = ctx.author_member().await; + + cmd_check_music_internal(member, channel_id, ctx).await +} + +pub async fn cmd_check_music_internal( + member: Option>, + channel_id: ChannelId, + ctx: Context<'_>, +) -> Result { + let guild_id = match ctx.guild_id() { + Some(id) => id, + None => { + tracing::warn!("No guild id found"); + return Ok(false); + }, + }; + + let guild_settings = match ctx.data().get_guild_settings(guild_id).await { + Some(guild_settings) => { + //let command_channel = guild_settings.command_channels.music_channel; + guild_settings + }, + None => return is_authorized_music(member, None), + }; + let opt_allowed_channel = guild_settings.get_music_channel(); + match opt_allowed_channel { + Some(allowed_channel) => { + if channel_id == allowed_channel { + is_authorized_music(member.clone(), None) + } else { + // Ok(false) + Err(CrackedError::NotInMusicChannel(channel_id).into()) + } + }, + None => is_authorized_music(member, None), + } +} + +/// Check if the user is authorized to use the music commands. +pub fn is_authorized_music( + member: Option>, + role: Option, +) -> Result { + let member = match member { + Some(m) => m, + None => { + tracing::warn!("No member found"); + return Ok(true); + }, + }; + // implementation of the is_authorized_music function + // ... + let perms = member.permissions.unwrap_or_default(); + let has_role = role + .map(|x| member.roles.contains(x.as_ref())) + .unwrap_or(true); + let is_admin = perms.contains(Permissions::ADMINISTRATOR); + + Ok(is_admin || has_role) + // true // placeholder return value +} diff --git a/crack-core/src/commands/ping.rs b/crack-core/src/commands/ping.rs deleted file mode 100644 index 98b37b39a..000000000 --- a/crack-core/src/commands/ping.rs +++ /dev/null @@ -1,25 +0,0 @@ -use poise::CreateReply; - -use crate::{Context, Error}; - -/// Ping the bot -#[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command)] -pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { - ping_internal(ctx).await -} - -/// Ping the bot internal function -#[cfg(not(tarpaulin_include))] -pub async fn ping_internal(ctx: Context<'_>) -> Result<(), Error> { - let start = std::time::Instant::now(); - let msg = ctx.say("Pong!").await?; - let end = std::time::Instant::now(); - let _ = msg - .edit( - ctx, - CreateReply::default().content(format!("Pong! ({}ms)", (end - start).as_millis())), - ) - .await; - Ok(()) -} diff --git a/crack-core/src/commands/playlist/add_to_playlist.rs b/crack-core/src/commands/playlist/add_to_playlist.rs index bc839881d..08df18c12 100644 --- a/crack-core/src/commands/playlist/add_to_playlist.rs +++ b/crack-core/src/commands/playlist/add_to_playlist.rs @@ -1,8 +1,9 @@ use crate::{ commands::MyAuxMetadata, db::aux_metadata_to_db_structures, - db::{metadata::Metadata, Playlist}, + db::{metadata::Metadata, MetadataAnd, Playlist}, errors::CrackedError, + poise_ext::ContextExt as _, Context, Error, }; use sqlx::PgPool; @@ -38,14 +39,9 @@ pub async fn add_to_playlist( Some(track) => track, None => metadata.title.clone().ok_or(CrackedError::NoTrackName)?, }; - //.unwrap_or(metadata.title.clone().ok_or(CrackedError::NoTrackName)?) let user_id = ctx.author().id.get() as i64; // Database pool to execute queries - let db_pool: PgPool = ctx - .data() - .database_pool - .clone() - .ok_or(CrackedError::NoDatabasePool)?; + let db_pool: PgPool = ctx.get_db_pool()?; let playlist_name = playlist; // Get playlist if exists, other create it. @@ -59,7 +55,7 @@ pub async fn add_to_playlist( }, }?; - let (in_metadata, _playlist_track) = + let MetadataAnd::Track(in_metadata, _) = aux_metadata_to_db_structures(metadata, guild_id_i64, channel_id)?; let metadata = Metadata::get_or_create(&db_pool, &in_metadata).await?; diff --git a/crack-core/src/commands/playlist/create_playlist.rs b/crack-core/src/commands/playlist/create_playlist.rs index a979cea14..cb675d466 100644 --- a/crack-core/src/commands/playlist/create_playlist.rs +++ b/crack-core/src/commands/playlist/create_playlist.rs @@ -1,6 +1,5 @@ use crate::{ - db::playlist::Playlist, messaging::message::CrackedMessage, utils::send_response_poise, - Context, Error, + db::playlist::Playlist, messaging::message::CrackedMessage, utils::send_reply, Context, Error, }; /// Creates a playlist @@ -12,8 +11,8 @@ pub async fn create_playlist(ctx: Context<'_>, name: String) -> Result<(), Error let res = Playlist::create(ctx.data().database_pool.as_ref().unwrap(), &name, user_id).await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::PlaylistCreated(res.name.clone(), 0), true, ) diff --git a/crack-core/src/commands/playlist/delete_playlist.rs b/crack-core/src/commands/playlist/delete_playlist.rs index 2e251f8da..25427175c 100644 --- a/crack-core/src/commands/playlist/delete_playlist.rs +++ b/crack-core/src/commands/playlist/delete_playlist.rs @@ -1,6 +1,5 @@ use crate::{ - db::playlist::Playlist, messaging::message::CrackedMessage, utils::send_response_poise, - Context, Error, + db::playlist::Playlist, messaging::message::CrackedMessage, utils::send_reply, Context, Error, }; /// Deletes a playlist @@ -13,8 +12,8 @@ pub async fn delete_playlist(ctx: Context<'_>, playlist_id: i32) -> Result<(), E Playlist::delete_playlist_by_id(pool, playlist_id, user_id).await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!( "Successfully deleted playlist with ID: {}", playlist_id diff --git a/crack-core/src/commands/playlist/get_playlist.rs b/crack-core/src/commands/playlist/get_playlist.rs index da492e785..8ec2ab2dd 100644 --- a/crack-core/src/commands/playlist/get_playlist.rs +++ b/crack-core/src/commands/playlist/get_playlist.rs @@ -14,7 +14,7 @@ pub async fn get_playlist(ctx: Context<'_>, #[rest] playlist: String) -> Result< let embed = build_tracks_embed_metadata(playlist_name, aux_metadata.as_slice(), 0).await; // Send the embed - send_embed_response_poise(ctx, embed).await?; + send_embed_response_poise(&ctx, embed).await?; Ok(()) } diff --git a/crack-core/src/commands/playlist/list_playlists.rs b/crack-core/src/commands/playlist/list_playlists.rs index 45e58ce7e..344c923d8 100644 --- a/crack-core/src/commands/playlist/list_playlists.rs +++ b/crack-core/src/commands/playlist/list_playlists.rs @@ -1,10 +1,21 @@ use crate::errors::CrackedError; use crate::utils::{build_playlist_list_embed, send_embed_response_poise}; -use crate::{db::playlist::Playlist, Context, Error}; +use crate::{ + commands::{cmd_check_music, sub_help as help}, + db::playlist::Playlist, + Context, Error, +}; -/// Get a playlist +/// List your saved playlists. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, rename = "list")] +#[poise::command( + category = "Music", + prefix_command, + slash_command, + rename = "list", + check = "cmd_check_music", + subcommands("help") +)] pub async fn list_playlists(ctx: Context<'_>) -> Result<(), Error> { let user_id = ctx.author().id.get() as i64; let pool = ctx @@ -17,7 +28,7 @@ pub async fn list_playlists(ctx: Context<'_>) -> Result<(), Error> { let embed = build_playlist_list_embed(&playlists, 0).await; // Send the embed - send_embed_response_poise(ctx, embed).await?; + send_embed_response_poise(&ctx, embed).await?; Ok(()) } diff --git a/crack-core/src/commands/playlist/loadspotify.rs b/crack-core/src/commands/playlist/loadspotify.rs index c5bc7b317..c82f5ede7 100644 --- a/crack-core/src/commands/playlist/loadspotify.rs +++ b/crack-core/src/commands/playlist/loadspotify.rs @@ -5,7 +5,7 @@ use crate::{ http_utils, messaging::message::CrackedMessage, sources::spotify::{Spotify, SpotifyTrack, SPOTIFY}, - utils::send_response_poise, + utils::send_reply, Context, CrackedError, Error, }; use songbird::input::AuxMetadata; @@ -41,6 +41,8 @@ pub async fn loadspotify_( name: String, spotifyurl: String, ) -> Result, Error> { + use crate::db::MetadataAnd; + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let channel_id = ctx.channel_id(); @@ -63,7 +65,7 @@ pub async fn loadspotify_( metadata_vec.push(m.clone()); let res = aux_metadata_to_db_structures(&m, guild_id_i64, channel_id_i64); match res { - Ok((in_metadata, _track)) => { + Ok(MetadataAnd::Track(in_metadata, _)) => { let metadata = Metadata::get_or_create(db_pool, &in_metadata).await?; let _res = Playlist::add_track( @@ -101,7 +103,7 @@ pub async fn loadspotify( let len = metadata_vec.len(); // Send the embed - send_response_poise(ctx, CrackedMessage::PlaylistCreated(name, len), false).await?; + send_reply(&ctx, CrackedMessage::PlaylistCreated(name, len), false).await?; Ok(()) } diff --git a/crack-core/src/commands/playlist/mod.rs b/crack-core/src/commands/playlist/mod.rs index 3b03807a8..6a568078a 100644 --- a/crack-core/src/commands/playlist/mod.rs +++ b/crack-core/src/commands/playlist/mod.rs @@ -14,20 +14,52 @@ pub use list_playlists::list_playlists as list; pub use loadspotify::loadspotify; pub use play_playlist::play_playlist as play; -use crate::{Context, Error}; +use crate::{ + commands::{cmd_check_music, sub_help as help}, + messaging::message::CrackedMessage, + utils::send_reply, + Context, Error, +}; /// Playlist commands. #[poise::command( + category = "Music", prefix_command, slash_command, - subcommands("addto", "create", "delete", "get", "list", "play", "loadspotify"), - aliases("pl") + subcommands( + "addto", + "create", + "delete", + "get", + "list", + "play", + "loadspotify", + "help" + ), + aliases("pl"), + check = "cmd_check_music" )] #[cfg(not(tarpaulin_include))] pub async fn playlist(ctx: Context<'_>) -> Result<(), Error> { - tracing::warn!("Playlist command called"); - - ctx.say("You found the playlist command").await?; + send_reply( + &ctx, + CrackedMessage::Other("You found the playlist command! Try /playlist help.".to_string()), + true, + ) + .await?; Ok(()) } + +pub fn commands() -> [crate::Command; 1] { + [playlist()] + // [ + // addto(), + // create(), + // delete(), + // get(), + // list(), + // play(), + // loadspotify(), + // ] +} diff --git a/crack-core/src/commands/playlist/play_playlist.rs b/crack-core/src/commands/playlist/play_playlist.rs index cceeb8536..5387ff8bb 100644 --- a/crack-core/src/commands/playlist/play_playlist.rs +++ b/crack-core/src/commands/playlist/play_playlist.rs @@ -1,10 +1,10 @@ use super::get_playlist::get_playlist_; use crate::commands::queue_aux_metadata; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::{Context, Error}; -/// Get a playlist +/// Queue a playlist on the bot. #[cfg(not(tarpaulin_include))] #[poise::command(prefix_command, slash_command, rename = "play")] pub async fn play_playlist( @@ -16,12 +16,12 @@ pub async fn play_playlist( // Check for playlist Id let (aux_metadata, playlist_name) = get_playlist_(ctx, playlist).await?; - let msg = - send_response_poise(ctx, CrackedMessage::PlaylistQueuing(playlist_name), true).await?; + let handle = send_reply(&ctx, CrackedMessage::PlaylistQueuing(playlist_name), true).await?; + let msg = handle.into_message().await?; queue_aux_metadata(ctx, aux_metadata.as_slice(), msg).await?; - send_response_poise(ctx, CrackedMessage::PlaylistQueued, true).await?; + send_reply(&ctx, CrackedMessage::PlaylistQueued, true).await?; Ok(()) } diff --git a/crack-core/src/commands/register.rs b/crack-core/src/commands/register.rs new file mode 100644 index 000000000..728cdc530 --- /dev/null +++ b/crack-core/src/commands/register.rs @@ -0,0 +1,270 @@ +//! Utilities for registering application commands + +use poise::serenity_prelude as serenity; + +/// Collects all commands into a [`Vec`] builder, which can be used +/// to register the commands on Discord +/// +/// Also see [`register_application_commands_buttons`] for a ready to use register command +/// +/// ```rust,no_run +/// # use poise::serenity_prelude as serenity; +/// # async fn foo(ctx: poise::Context<'_, (), ()>) -> Result<(), serenity::Error> { +/// let commands = &ctx.framework().options().commands; +/// let create_commands = poise::builtins::create_application_commands(commands); +/// +/// serenity::Command::set_global_commands(ctx, create_commands).await?; +/// # Ok(()) } +/// ``` +pub fn create_application_commands_cracked( + commands: &[poise::Command], +) -> Vec { + /// We decided to extract context menu commands recursively, despite the subcommand hierarchy + /// not being preserved. Because it's more confusing to just silently discard context menu + /// commands if they're not top-level commands. + /// https://discord.com/channels/381880193251409931/919310428344029265/947970605985189989 + fn recursively_add_context_menu_commands( + builder: &mut Vec, + command: &poise::Command, + ) { + if let Some(context_menu_command) = command.create_as_context_menu_command() { + builder.push(context_menu_command); + } + for subcommand in &command.subcommands { + if subcommand.name != "help" { + recursively_add_context_menu_commands(builder, subcommand); + } + } + } + + let mut commands_builder = Vec::with_capacity(commands.len()); + for command in commands { + if let Some(slash_command) = command.create_as_slash_command() { + commands_builder.push(slash_command); + } + recursively_add_context_menu_commands(&mut commands_builder, command); + } + commands_builder +} + +/// Registers the given list of application commands to Discord as global commands. +/// +/// Thin wrapper around [`create_application_commands`] that funnels the returned builder into +/// [`serenity::Command::set_global_commands`]. +#[allow(dead_code)] +pub async fn register_globally_cracked( + http: impl AsRef, + commands: &[poise::Command], +) -> Result<(), serenity::Error> { + let builder = create_application_commands_cracked(commands); + serenity::Command::set_global_commands(http, builder).await?; + Ok(()) +} + +/// Registers the given list of application commands to Discord as guild-specific commands. +/// +/// Thin wrapper around [`create_application_commands`] that funnels the returned builder into +/// [`serenity::GuildId::set_commands`]. +#[allow(dead_code)] +pub async fn register_in_guild_cracked( + http: impl AsRef, + commands: &[poise::Command], + guild_id: serenity::GuildId, +) -> Result<(), serenity::Error> { + let builder = create_application_commands_cracked(commands); + guild_id.set_commands(http, builder).await?; + Ok(()) +} + +/// _Note: you probably want [`register_application_commands_buttons`] instead; it's easier and more +/// powerful_ +/// +/// Wraps [`create_application_commands`] and adds a bot owner permission check and status messages. +/// +/// This function is supposed to be a ready-to-use implementation for a `~register` command of your +/// bot. So if you want, you can copy paste this help message for the command: +/// +/// ```text +/// Registers application commands in this guild or globally +/// +/// Run with no arguments to register in guild, run with argument "global" to register globally. +/// ``` +#[allow(dead_code)] +pub async fn register_application_commands_cracked( + ctx: poise::Context<'_, U, E>, + global: bool, +) -> Result<(), serenity::Error> { + let is_bot_owner = ctx.framework().options().owners.contains(&ctx.author().id); + if !is_bot_owner { + ctx.say("Can only be used by bot owner").await?; + return Ok(()); + } + + let commands_builder = create_application_commands_cracked(&ctx.framework().options().commands); + let num_commands = commands_builder.len(); + + if global { + ctx.say(format!("Registering {num_commands} commands...",)) + .await?; + serenity::Command::set_global_commands(ctx, commands_builder).await?; + } else { + let guild_id = match ctx.guild_id() { + Some(x) => x, + None => { + ctx.say("Must be called in guild").await?; + return Ok(()); + }, + }; + + ctx.say(format!("Registering {num_commands} commands...")) + .await?; + guild_id.set_commands(ctx, commands_builder).await?; + } + + ctx.say("Done!").await?; + + Ok(()) +} + +/// Spawns four buttons to register or delete application commands globally or in the current guild +/// +/// Upgraded version of [`register_application_commands`] +/// +/// ![Screenshot of output](https://imgur.com/rTbTaDs.png) +/// +/// You probably want to use this by wrapping it in a small `register` command: +/// ```rust +/// # type Error = Box; +/// # type Context<'a> = poise::Context<'a, (), Error>; +/// #[poise::command(prefix_command)] +/// pub async fn register(ctx: Context<'_>) -> Result<(), Error> { +/// poise::builtins::register_application_commands_buttons(ctx).await?; +/// Ok(()) +/// } +/// +/// // ... +/// poise::FrameworkOptions { +/// commands: vec![ +/// // ... +/// register(), +/// ], +/// # ..Default::default() +/// }; +/// ``` +/// +/// Which you can call like any prefix command, for example `@your_bot register`. +pub async fn register_application_commands_buttons_cracked( + ctx: poise::Context<'_, U, E>, +) -> Result<(), serenity::Error> { + let create_commands = create_application_commands_cracked(&ctx.framework().options().commands); + let num_commands = create_commands.len(); + + let is_bot_owner = ctx.framework().options().owners.contains(&ctx.author().id); + if !is_bot_owner { + ctx.say("Can only be used by bot owner").await?; + return Ok(()); + } + + let components = serenity::CreateActionRow::Buttons(vec![ + serenity::CreateButton::new("register.guild") + .label("Register in guild") + .style(serenity::ButtonStyle::Primary) + .emoji('📋'), + serenity::CreateButton::new("unregister.guild") + .label("Delete in guild") + .style(serenity::ButtonStyle::Danger) + .emoji('🗑'), + serenity::CreateButton::new("register.global") + .label("Register globally") + .style(serenity::ButtonStyle::Primary) + .emoji('📋'), + serenity::CreateButton::new("unregister.global") + .label("Unregister globally") + .style(serenity::ButtonStyle::Danger) + .emoji('🗑'), + ]); + + let builder = poise::CreateReply::default() + .content("Choose what to do with the commands:") + .components(vec![components]); + + let reply = ctx.send(builder).await?; + + let interaction = reply + .message() + .await? + .await_component_interaction(ctx) + .author_id(ctx.author().id) + .await; + + reply + .edit( + ctx, + poise::CreateReply::default() + .components(vec![]) + .content("Processing... Please wait."), + ) + .await?; // remove buttons after button press and edit message + let pressed_button_id = match &interaction { + Some(m) => &m.data.custom_id, + None => { + ctx.say(":warning: You didn't interact in time - please run the command again.") + .await?; + return Ok(()); + }, + }; + + let (register, global) = match &**pressed_button_id { + "register.global" => (true, true), + "unregister.global" => (false, true), + "register.guild" => (true, false), + "unregister.guild" => (false, false), + other => { + tracing::warn!("unknown register button ID: {:?}", other); + return Ok(()); + }, + }; + + let start_time = std::time::Instant::now(); + + if global { + if register { + ctx.say(format!( + ":gear: Registering {num_commands} global commands...", + )) + .await?; + serenity::Command::set_global_commands(ctx, create_commands).await?; + } else { + ctx.say(":gear: Unregistering global commands...").await?; + serenity::Command::set_global_commands(ctx, vec![]).await?; + } + } else { + let guild_id = match ctx.guild_id() { + Some(x) => x, + None => { + ctx.say(":x: Must be called in guild").await?; + return Ok(()); + }, + }; + if register { + ctx.say(format!( + ":gear: Registering {num_commands} guild commands...", + )) + .await?; + guild_id.set_commands(ctx, create_commands).await?; + } else { + ctx.say(":gear: Unregistering guild commands...").await?; + guild_id.set_commands(ctx, vec![]).await?; + } + } + + // Calulate time taken and send message + let time_taken = start_time.elapsed(); + ctx.say(format!( + ":white_check_mark: Done! Took {}ms", + time_taken.as_millis() + )) + .await?; + + Ok(()) +} diff --git a/crack-core/src/commands/settings/get/all.rs b/crack-core/src/commands/settings/get/all.rs index 2f52ced2f..db4923f5b 100644 --- a/crack-core/src/commands/settings/get/all.rs +++ b/crack-core/src/commands/settings/get/all.rs @@ -1,25 +1,34 @@ +use crate::commands::CrackedError; use crate::guild::settings::GuildSettings; use crate::messaging::message::CrackedMessage; use crate::utils::get_guild_name; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::{Context, Error}; /// Get the current bot settings for this guild. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", - ephemeral, aliases("get_all_settings") )] -pub async fn all(ctx: Context<'_>) -> Result<(), Error> { +pub async fn all( + ctx: Context<'_>, + #[flag] + #[description = "Shows the help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + crate::commands::help::wrapper(ctx).await?; + } get_settings(ctx).await } /// Get the current bot settings for this guild. pub async fn get_settings(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let settings_ro = { let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map @@ -27,13 +36,13 @@ pub async fn get_settings(ctx: Context<'_>) -> Result<(), Error> { .or_insert(GuildSettings::new( guild_id, Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), + get_guild_name(ctx.serenity_context(), guild_id).await, )); settings.clone() }; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Settings: {:?}", settings_ro)), true, ) diff --git a/crack-core/src/commands/settings/get/get_auto_role.rs b/crack-core/src/commands/settings/get/get_auto_role.rs index f017b2feb..688bcf700 100644 --- a/crack-core/src/commands/settings/get/get_auto_role.rs +++ b/crack-core/src/commands/settings/get/get_auto_role.rs @@ -1,38 +1,37 @@ use serenity::all::GuildId; use serenity::model::id::RoleId; -use crate::{messaging::message::CrackedMessage, utils::send_response_poise}; +use crate::commands::CrackedError; +use crate::guild::operations::GuildSettingsOperations; +use crate::messaging::message::CrackedMessage; +use crate::poise_ext::MessageInterfaceCtxExt; use crate::{Context, Data, Error}; /// Get the auto role for the server. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", - ephemeral, - aliases("get_auto_role") + aliases("auto_role") )] -pub async fn auto_role(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); +pub async fn get_auto_role( + ctx: Context<'_>, + #[flag] + #[description = "Show the help menu for this command."] + flag: bool, +) -> Result<(), Error> { + if flag { + return crate::commands::help::wrapper(ctx).await; + } + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let data = ctx.data(); get_auto_role_internal(data, guild_id) .await .map_or_else( - || { - send_response_poise( - ctx, - CrackedMessage::Other("No auto role set for this server.".to_string()), - true, - ) - }, - |role_id| { - send_response_poise( - ctx, - CrackedMessage::Other(format!("Auto role: <@&{}>", role_id)), - true, - ) - }, + || ctx.send_reply(CrackedMessage::NoAutoRole, true), + |role_id| ctx.send_reply(CrackedMessage::AutoRole(role_id), true), ) .await .map(|_| ()) @@ -41,10 +40,5 @@ pub async fn auto_role(ctx: Context<'_>) -> Result<(), Error> { /// Get the auto role for the server. pub async fn get_auto_role_internal(data: &Data, guild_id: GuildId) -> Option { - let guild_settings_map = data.guild_settings_map.read().await; - let guild_settings = guild_settings_map.get(&guild_id)?; - guild_settings - .welcome_settings - .as_ref() - .map(|x| x.auto_role.map(|y| y.into()).unwrap_or_default()) + data.get_auto_role(guild_id).await.map(RoleId::from) } diff --git a/crack-core/src/commands/settings/get/get_idle_timeout.rs b/crack-core/src/commands/settings/get/get_idle_timeout.rs index 36aa8e0f2..0c741cc6a 100644 --- a/crack-core/src/commands/settings/get/get_idle_timeout.rs +++ b/crack-core/src/commands/settings/get/get_idle_timeout.rs @@ -1,37 +1,42 @@ -use crate::guild::settings::GuildSettings; +use crate::guild::operations::GuildSettingsOperations; +use crate::http_utils::SendMessageParams; use crate::messaging::message::CrackedMessage; -use crate::utils::get_guild_name; -use crate::utils::send_response_poise; +use crate::poise_ext::PoiseContextExt; use crate::{Context, Error}; #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", - ephemeral, aliases("get_idle_timeout") )] -pub async fn idle_timeout(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let idle_timeout = { - let mut guild_settings_map = ctx.data().guild_settings_map.write().await; - let settings = guild_settings_map - .entry(guild_id) - .or_insert(GuildSettings::new( - guild_id, - Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), - )); - settings.timeout - }; +pub async fn idle_timeout( + ctx: Context<'_>, + #[flag] + #[description = "Shows the help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } + idle_timeout_internal(ctx).await +} + +/// Get the idle timeout for the bot +pub async fn idle_timeout_internal(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx + .guild_id() + .ok_or(crate::commands::CrackedError::NoGuildId)?; - send_response_poise( - ctx, - CrackedMessage::Other(format!("Idle timeout: {:?}s", idle_timeout)), - true, - ) - .await?; + let idle_timeout = ctx.data().get_timeout(guild_id).await; - Ok(()) + let params = SendMessageParams::new(CrackedMessage::Other( + format!("Idle timeout: {:?}s", idle_timeout).to_string(), + )); + ctx.send_message(params) + .await + .map_err(Into::into) + .map(|_| ()) } diff --git a/crack-core/src/commands/settings/get/get_premium.rs b/crack-core/src/commands/settings/get/get_premium.rs index 2d5035bb3..39de4d7d8 100644 --- a/crack-core/src/commands/settings/get/get_premium.rs +++ b/crack-core/src/commands/settings/get/get_premium.rs @@ -1,29 +1,29 @@ -use serenity::all::GuildId; - -use crate::{Context, Data, Error}; - -/// Get the current `premium` setting for the guild. -pub async fn get_premium(data: &Data, guild_id: GuildId) -> bool { - let guild_settings_map = data.guild_settings_map.read().await; - let guild_settings = guild_settings_map.get(&guild_id).unwrap(); - guild_settings.premium -} +use crate::guild::operations::GuildSettingsOperations; +use crate::poise_ext::PoiseContextExt; +use crate::CrackedError; +use crate::CrackedMessage; +use crate::{Context, Error}; /// Get the current `premium` setting for the guild. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, required_permissions = "ADMINISTRATOR", - ephemeral, aliases("get_premium_status") )] pub async fn premium(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + premium_internal(ctx).await +} + +/// Get the current `premium` setting for the guild. +pub async fn premium_internal(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let data = ctx.data(); - let res = get_premium(data, guild_id).await; + let res = data.get_premium(guild_id).await.unwrap_or(false); - ctx.say(format!("Premium status: {}", res)) + ctx.send_reply(CrackedMessage::Premium(res), true) .await .map_err(|e| e.into()) .map(|_| ()) diff --git a/crack-core/src/commands/settings/get/get_volume.rs b/crack-core/src/commands/settings/get/get_volume.rs index 9087d7269..a2a5beb16 100644 --- a/crack-core/src/commands/settings/get/get_volume.rs +++ b/crack-core/src/commands/settings/get/get_volume.rs @@ -1,58 +1,26 @@ -use crate::{guild::settings::GuildSettings, Context, Error}; -use serenity::all::GuildId; -use std::{collections::HashMap, sync::Arc}; -use tokio::sync::RwLock; - -/// Get the current `volume` and `old_volume` setting for the guild. -pub async fn get_volume( - guild_settings_map: Arc>>, - guild_id: GuildId, -) -> (f32, f32) { - let guild_settings_map = guild_settings_map.read().await; - let guild_settings = guild_settings_map.get(&guild_id).unwrap(); - (guild_settings.volume, guild_settings.old_volume) -} +use crate::guild::operations::GuildSettingsOperations; +use crate::messaging::message::CrackedMessage; +use crate::poise_ext::PoiseContextExt; +use crate::CrackedError; +use crate::{Context, Error}; /// Get the current `volume` and `old_volume` setting for the guild. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, - required_permissions = "ADMINISTRATOR", - ephemeral + guild_only, + required_permissions = "ADMINISTRATOR" )] -pub async fn volume(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); +pub async fn get_volume(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let data = ctx.data(); - let (vol, old_vol) = get_volume(data.guild_settings_map.clone(), guild_id).await; + let (vol, old_vol) = data.get_volume(guild_id).await; - ctx.say(format!("vol: {}, old_vol: {}", vol, old_vol)) + let volume = CrackedMessage::Volume { vol, old_vol }; + ctx.send_reply(volume, true) .await .map_err(|e| e.into()) .map(|_| ()) } - -#[cfg(test)] -mod test { - use super::get_volume; - use super::{Arc, RwLock}; - use crate::guild::settings::GuildSettings; - use serenity::model::id::GuildId; - - use crate::guild::settings::DEFAULT_VOLUME_LEVEL; - - #[tokio::test] - async fn test_volume() { - let guild_settings_map = Arc::new(RwLock::new(std::collections::HashMap::new())); - - let guild_id = GuildId::new(1); - let _guild_settings = guild_settings_map - .write() - .await - .entry(guild_id) - .or_insert(GuildSettings::new(guild_id, Some("!"), None)); - let (vol, old_vol) = get_volume(guild_settings_map, guild_id).await; - assert_eq!(vol, DEFAULT_VOLUME_LEVEL); - assert_eq!(old_vol, DEFAULT_VOLUME_LEVEL); - } -} diff --git a/crack-core/src/commands/settings/get/get_welcome_settings.rs b/crack-core/src/commands/settings/get/get_welcome_settings.rs index 4c60fcfd9..86b4ba505 100644 --- a/crack-core/src/commands/settings/get/get_welcome_settings.rs +++ b/crack-core/src/commands/settings/get/get_welcome_settings.rs @@ -1,16 +1,11 @@ use crate::guild::settings::GuildSettings; use crate::messaging::message::CrackedMessage; +use crate::poise_ext::PoiseContextExt; use crate::utils::get_guild_name; -use crate::utils::send_response_poise; use crate::{Context, Error}; #[cfg(not(tarpaulin_include))] -#[poise::command( - prefix_command, - owners_only, - ephemeral, - aliases("get_welcome_settings") -)] +#[poise::command(category = "Settings", slash_command, prefix_command, owners_only)] pub async fn welcome_settings(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let welcome_settings = { @@ -20,17 +15,13 @@ pub async fn welcome_settings(ctx: Context<'_>) -> Result<(), Error> { .or_insert(GuildSettings::new( guild_id, Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), + get_guild_name(ctx.serenity_context(), guild_id).await, )); settings.welcome_settings.clone() }; - send_response_poise( - ctx, - CrackedMessage::Other(format!( - "Welcome settings: {:?}", - welcome_settings.unwrap_or_default() - )), + ctx.send_reply( + CrackedMessage::WelcomeSettings(welcome_settings.unwrap_or_default().to_string()), true, ) .await?; diff --git a/crack-core/src/commands/settings/get/log_channels.rs b/crack-core/src/commands/settings/get/log_channels.rs index 4bda200ca..24a91c9f2 100644 --- a/crack-core/src/commands/settings/get/log_channels.rs +++ b/crack-core/src/commands/settings/get/log_channels.rs @@ -2,15 +2,15 @@ use crate::errors::CrackedError; use crate::guild::settings::GuildSettings; use crate::messaging::message::CrackedMessage; use crate::utils::get_guild_name; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::{Context, Error}; /// Get the all log channel. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, - ephemeral, guild_only, required_permissions = "ADMINISTRATOR", aliases("get_all_log_channel") @@ -25,13 +25,13 @@ pub async fn all_log_channel(ctx: Context<'_>) -> Result<(), Error> { .or_insert(GuildSettings::new( guild_id, Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), + get_guild_name(ctx.serenity_context(), guild_id).await, )); settings.get_all_log_channel() }; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!( "All Log Channel: {:?}", all_log_channel.unwrap_or_default() @@ -47,30 +47,27 @@ pub async fn all_log_channel(ctx: Context<'_>) -> Result<(), Error> { /// Get the join/leave log channel. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", slash_command, prefix_command, - required_permissions = "ADMINISTRATOR", - ephemeral + required_permissions = "ADMINISTRATOR" )] pub async fn join_leave_log_channel(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx .guild_id() .ok_or(crate::errors::CrackedError::NoGuildId)?; + let name = get_guild_name(ctx.serenity_context(), guild_id).await; { let join_leave_log_channel = { let mut guild_settings_map = ctx.data().guild_settings_map.write().await; let settings = guild_settings_map .entry(guild_id) - .or_insert(GuildSettings::new( - guild_id, - Some(ctx.prefix()), - get_guild_name(ctx.serenity_context(), guild_id), - )); + .or_insert(GuildSettings::new(guild_id, Some(ctx.prefix()), name)); settings.get_all_log_channel() }; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!( "Join/Leave Log Channel: {:?}", join_leave_log_channel.unwrap_or_default() diff --git a/crack-core/src/commands/settings/get/mod.rs b/crack-core/src/commands/settings/get/mod.rs index 7649e2516..d39d66107 100644 --- a/crack-core/src/commands/settings/get/mod.rs +++ b/crack-core/src/commands/settings/get/mod.rs @@ -31,14 +31,13 @@ pub use log_channels::*; subcommands( "all", "all_log_channel", - "auto_role", + "get_auto_role", "premium", "join_leave_log_channel", "welcome_settings", "idle_timeout", - "volume", - // "self_deafen", - ), + "get_volume", + ) )] /// Get settings @@ -50,3 +49,16 @@ pub async fn get(ctx: Context<'_>) -> Result<(), Error> { Ok(()) } + +pub fn commands() -> [crate::Command; 8] { + [ + all(), + all_log_channel(), + get_auto_role(), + premium(), + join_leave_log_channel(), + welcome_settings(), + idle_timeout(), + get_volume(), + ] +} diff --git a/crack-core/src/commands/settings/mod.rs b/crack-core/src/commands/settings/mod.rs index 4b38d7806..3ebda14f7 100644 --- a/crack-core/src/commands/settings/mod.rs +++ b/crack-core/src/commands/settings/mod.rs @@ -1,4 +1,5 @@ -use crate::{Context, Error}; +use crate::poise_ext::MessageInterfaceCtxExt; +use crate::{Context, CrackedMessage, Error}; pub mod get; pub mod prefix; @@ -6,6 +7,7 @@ pub mod print_settings; pub mod set; pub mod toggle; +use crate::commands::help; pub use get::get; pub use prefix::*; pub use print_settings::*; @@ -28,10 +30,34 @@ pub use toggle::*; required_permissions = "ADMINISTRATOR" )] #[cfg(not(tarpaulin_include))] -pub async fn settings(ctx: Context<'_>) -> Result<(), Error> { - tracing::warn!("Settings command called"); +pub async fn settings( + ctx: Context<'_>, + #[flag] + #[description = "Shows the help menu for this command"] + help: bool, +) -> Result<(), Error> { + if help { + help::wrapper(ctx).await?; + } - ctx.say("You found the settings command").await?; + ctx.send_reply(CrackedMessage::CommandFound(String::from("settings")), true) + .await?; Ok(()) } + +pub fn commands() -> Vec { + vec![settings()] + .into_iter() + //.chain(set::commands()) + //.chain(get::commands()) + .collect() +} + +pub fn sub_commands() -> Vec { + vec![] + .into_iter() + .chain(set::commands()) + .chain(get::commands()) + .collect() +} diff --git a/crack-core/src/commands/settings/prefix.rs b/crack-core/src/commands/settings/prefix.rs index 6503c3c5b..590b683cb 100644 --- a/crack-core/src/commands/settings/prefix.rs +++ b/crack-core/src/commands/settings/prefix.rs @@ -1,7 +1,7 @@ use crate::guild::settings::GuildSettings; +use crate::http_utils::CacheHttpExt; use crate::messaging::message::CrackedMessage; -use crate::utils::get_guild_name; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; @@ -12,8 +12,10 @@ pub async fn add_prefix( ctx: Context<'_>, #[description = "The prefix to add to the bot"] prefix: String, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let guild_name = get_guild_name(ctx.serenity_context(), guild_id).unwrap_or_default(); + use crate::{commands::CrackedError, http_utils::CacheHttpExt}; + + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = ctx.guild_name_from_guild_id(guild_id).await?; let additional_prefixes = { let mut settings = ctx.data().guild_settings_map.write().await; let new_settings = settings @@ -33,8 +35,8 @@ pub async fn add_prefix( )); new_settings.additional_prefixes.clone() }; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!( "Current additional prefixes {}", additional_prefixes.join(", ") @@ -52,8 +54,10 @@ pub async fn clear_prefixes( ctx: Context<'_>, #[description = "The prefix to add to the bot"] prefix: String, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let guild_name = get_guild_name(ctx.serenity_context(), guild_id).unwrap_or_default(); + use crate::commands::CrackedError; + + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = ctx.guild_name_from_guild_id(guild_id).await?; let additional_prefixes = { let mut settings = ctx.data().guild_settings_map.write().await; let new_settings = settings @@ -68,8 +72,8 @@ pub async fn clear_prefixes( )); new_settings.additional_prefixes.clone() }; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!( "Current additional prefixes {}", additional_prefixes.join(", ") @@ -92,8 +96,8 @@ pub async fn get_prefixes(ctx: Context<'_>) -> Result<(), Error> { .map(|e| e.additional_prefixes.clone()) .unwrap_or_default() }; - let msg = send_response_poise( - ctx, + let _ = send_reply( + &ctx, CrackedMessage::Other(format!( "Current additional prefixes {}", additional_prefixes.join(", ") @@ -101,6 +105,5 @@ pub async fn get_prefixes(ctx: Context<'_>) -> Result<(), Error> { true, ) .await?; - ctx.data().add_msg_to_cache(guild_id, msg); Ok(()) } diff --git a/crack-core/src/commands/settings/print_settings.rs b/crack-core/src/commands/settings/print_settings.rs index 459a5e74f..6869a568f 100644 --- a/crack-core/src/commands/settings/print_settings.rs +++ b/crack-core/src/commands/settings/print_settings.rs @@ -1,6 +1,6 @@ use crate::{ - guild::settings::GuildSettingsMap, messaging::message::CrackedMessage, - utils::send_response_poise, Context, Error, + guild::settings::GuildSettingsMap, messaging::message::CrackedMessage, utils::send_reply, + Context, Error, }; use serenity::{ all::{Channel, Message, User}, @@ -12,8 +12,8 @@ pub async fn print_settings(ctx: Context<'_>) -> Result<(), Error> { let guild_settings_map = ctx.data().guild_settings_map.read().await.clone(); //.unwrap().clone(); for (guild_id, settings) in guild_settings_map.iter() { - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Settings for guild {}: {:?}", guild_id, settings)), true, ) @@ -23,8 +23,8 @@ pub async fn print_settings(ctx: Context<'_>) -> Result<(), Error> { let guild_settings_map = ctx.serenity_context().data.read().await; for (guild_id, settings) in guild_settings_map.get::().unwrap().iter() { - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Settings for guild {}: {:?}", guild_id, settings)), true, ) diff --git a/crack-core/src/commands/settings/set/mod.rs b/crack-core/src/commands/settings/set/mod.rs index 35258a7d3..88dd26765 100644 --- a/crack-core/src/commands/settings/set/mod.rs +++ b/crack-core/src/commands/settings/set/mod.rs @@ -1,3 +1,5 @@ +use crate::messaging::message::CrackedMessage; +use crate::poise_ext::MessageInterfaceCtxExt; use crate::{Context, Error}; // pub mod welcome; @@ -21,6 +23,7 @@ pub use set_welcome_settings::*; /// Settings-get commands #[poise::command( + category = "Settings", slash_command, prefix_command, subcommands( @@ -36,15 +39,29 @@ pub use set_welcome_settings::*; // "log_all", // "log_guild" ), - ephemeral, required_permissions = "ADMINISTRATOR", )] /// Set settings #[cfg(not(tarpaulin_include))] pub async fn set(ctx: Context<'_>) -> Result<(), Error> { - tracing::warn!(""); - - ctx.say("You found the settings-set command").await?; + ctx.send_reply( + CrackedMessage::CommandFound(String::from("settings-set")), + true, + ) + .await?; Ok(()) } + +pub fn commands() -> [crate::Command; 8] { + [ + all_log_channel(), + auto_role(), + idle_timeout(), + join_leave_log_channel(), + music_channel(), + premium(), + volume(), + welcome_settings(), + ] +} diff --git a/crack-core/src/commands/settings/set/set_all_log_channel.rs b/crack-core/src/commands/settings/set/set_all_log_channel.rs index 6b0fbca3a..fb7cb8730 100644 --- a/crack-core/src/commands/settings/set/set_all_log_channel.rs +++ b/crack-core/src/commands/settings/set/set_all_log_channel.rs @@ -1,11 +1,17 @@ +use crate::commands::CrackedError; use crate::guild::settings::{GuildSettings, DEFAULT_PREFIX}; use crate::Data; -use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crate::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; use serenity::all::{Channel, GuildId}; use serenity::model::id::ChannelId; /// Set a log channel for a specific guild. -#[poise::command(prefix_command, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES" +)] pub async fn log_channel_for_guild( ctx: Context<'_>, #[description = "GuildId to set logging for"] guild_id: GuildId, @@ -15,8 +21,8 @@ pub async fn log_channel_for_guild( // set_all_log_channel_old_data(ctx.serenity_context().data.clone(), guild_id, channel_id).await?; set_all_log_channel_data(ctx.data(), guild_id, channel_id).await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("all log channel set to {}", channel_id)), true, ) @@ -26,26 +32,37 @@ pub async fn log_channel_for_guild( } /// Set a channel to send all logs. -#[poise::command(prefix_command, owners_only)] +#[poise::command( + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES" +)] pub async fn all_log_channel( ctx: Context<'_>, #[description = "Channel to send all logs"] channel: Option, #[description = "ChannelId to send all logs"] channel_id: Option< serenity::model::id::ChannelId, >, + #[flag] + #[description = "Show the help menu for this command"] + help: bool, ) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } let channel_id = if let Some(channel) = channel { channel.id() + } else if let Some(channel_id) = channel_id { + channel_id } else { - channel_id.unwrap() + return Err(Box::new(CrackedError::Other("No channel provided"))); }; let guild_id = ctx.guild_id().unwrap(); - // set_all_log_channel_old_data(ctx.serenity_context().data.clone(), guild_id, channel_id).await?; set_all_log_channel_data(ctx.data(), guild_id, channel_id).await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("all log channel set to {}", channel_id)), true, ) diff --git a/crack-core/src/commands/settings/set/set_auto_role.rs b/crack-core/src/commands/settings/set/set_auto_role.rs index 6cc2a3e0a..bc0a027e2 100644 --- a/crack-core/src/commands/settings/set/set_auto_role.rs +++ b/crack-core/src/commands/settings/set/set_auto_role.rs @@ -1,14 +1,29 @@ +use poise::serenity_prelude::Mentionable; +use serenity::all::{Role, RoleId}; + use crate::{ - errors::CrackedError, guild::settings::GuildSettings, utils::get_guild_name, Context, Error, + errors::CrackedError, guild::operations::GuildSettingsOperations, + http_utils::SendMessageParams, messaging::message::CrackedMessage, poise_ext::PoiseContextExt, + Context, Error, }; /// Set the auto role for the server. -#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] -pub async fn auto_role( +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "MANAGE_ROLES" +)] +pub async fn auto_role_id( ctx: Context<'_>, - #[description = "The role to assign to new users"] auto_role_id_str: String, + #[description = "The id of the role to assign to new users."] auto_role_id_str: String, + #[flag] + #[description = "Show the help menu for this command"] + help: bool, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + if help { + return crate::commands::help::wrapper(ctx).await; + } let auto_role_id = match auto_role_id_str.parse::() { Ok(x) => x, Err(e) => { @@ -16,31 +31,51 @@ pub async fn auto_role( return Ok(()); }, }; + let role = RoleId::from(auto_role_id); + + auto_role_internal(ctx, role).await +} +/// Set the auto role for the server. +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "MANAGE_ROLES" +)] +pub async fn auto_role( + ctx: Context<'_>, + #[description = "The role to assign to new users"] auto_role: Role, + #[flag] + #[description = "Show the help menu for this command"] + help: bool, +) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } + auto_role_internal(ctx, auto_role.id).await +} + +/// Set the auto role for the server. +pub async fn auto_role_internal(ctx: Context<'_>, auto_role: RoleId) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let mention = auto_role.mention(); + + ctx.data().set_auto_role(guild_id, auto_role.get()).await; let res = ctx .data() - .guild_settings_map - .write() + .get_guild_settings(guild_id) .await - .entry(guild_id) - .and_modify(|e| { - e.set_auto_role(Some(auto_role_id)); - }) - .or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(&ctx.data().bot_settings.get_prefix()), - get_guild_name(ctx.serenity_context(), guild_id), - ) - .with_auto_role(Some(auto_role_id)) - }) - .welcome_settings - .clone(); - res.unwrap() - .save(&ctx.data().database_pool.clone().unwrap(), guild_id.get()) - .await?; + .ok_or(CrackedError::NoGuildSettings)?; + res.save(&ctx.data().database_pool.clone().unwrap()).await?; - ctx.say(format!("Auto role set to {}", auto_role_id)) - .await?; - Ok(()) + //ctx.say(format!("Auto role set to {}", mention)).await?; + let params = SendMessageParams::new(CrackedMessage::Other(format!( + "Auto role set to {}", + mention + ))); + ctx.send_message(params) + .await + .map(|_| ()) + .map_err(Into::into) } diff --git a/crack-core/src/commands/settings/set/set_idle_timeout.rs b/crack-core/src/commands/settings/set/set_idle_timeout.rs index 6c3cacc2b..6d20bdfe5 100644 --- a/crack-core/src/commands/settings/set/set_idle_timeout.rs +++ b/crack-core/src/commands/settings/set/set_idle_timeout.rs @@ -8,8 +8,8 @@ use poise::CreateReply; /// Set the idle timeout for the bot in vc. #[cfg(not(tarpaulin_include))] #[poise::command( + category = "Settings", prefix_command, - ephemeral, aliases("set_idle_timeout"), required_permissions = "ADMINISTRATOR" )] @@ -21,6 +21,7 @@ pub async fn idle_timeout( let data = ctx.data(); let timeout = timeout * 60; + let name = get_guild_name(ctx.serenity_context(), guild_id).await; let _res = data .guild_settings_map @@ -29,13 +30,9 @@ pub async fn idle_timeout( .entry(guild_id) .and_modify(|e| e.timeout = timeout) .or_insert_with(|| { - GuildSettings::new( - guild_id, - Some(&ctx.data().bot_settings.get_prefix()), - get_guild_name(ctx.serenity_context(), guild_id), - ) - .with_timeout(timeout) - .clone() + GuildSettings::new(guild_id, Some(&ctx.data().bot_settings.get_prefix()), name) + .with_timeout(timeout) + .clone() }) .welcome_settings .clone(); diff --git a/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs b/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs index fa77f70d2..8c1a9c8f0 100644 --- a/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs +++ b/crack-core/src/commands/settings/set/set_join_leave_log_channel.rs @@ -1,13 +1,18 @@ use crate::errors::CrackedError; use crate::guild::operations::GuildSettingsOperations; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::Context; use crate::Error; use serenity::all::Channel; /// Set the join-leave log channel. -#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES" +)] pub async fn join_leave_log_channel( ctx: Context<'_>, #[description = "Channel to send join/leave logs"] channel: Option, @@ -55,8 +60,8 @@ pub async fn join_leave_log_channel( let pg_pool = ctx.data().database_pool.clone().unwrap(); settings.map(|s| s.save(&pg_pool)).unwrap().await?; - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::Other(format!("Join-leave log channel set to {}", channel_id)), true, ) diff --git a/crack-core/src/commands/settings/set/set_music_channel.rs b/crack-core/src/commands/settings/set/set_music_channel.rs index fe8993b36..125e171eb 100644 --- a/crack-core/src/commands/settings/set/set_music_channel.rs +++ b/crack-core/src/commands/settings/set/set_music_channel.rs @@ -1,18 +1,28 @@ use crate::guild::operations::GuildSettingsOperations; use crate::{ - errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, - Error, + errors::CrackedError, messaging::message::CrackedMessage, utils::send_reply, Context, Error, }; use serenity::all::Channel; -#[poise::command(prefix_command, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES" +)] pub async fn music_channel( ctx: Context<'_>, #[description = "Channel to respond to music commands in."] channel: Option, #[description = "ChannelId of Channel to respond to music commands in."] channel_id: Option< serenity::model::id::ChannelId, >, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, ) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } if channel.is_none() && channel_id.is_none() { return Err(CrackedError::Other("Must provide either a channel or a channel id").into()); } @@ -30,16 +40,44 @@ pub async fn music_channel( let opt_settings = data.guild_settings_map.read().await.clone(); let settings = opt_settings.get(&guild_id); + // FIXME: Do this with the async work queue. let pg_pool = ctx.data().database_pool.clone().unwrap(); settings.map(|s| s.save(&pg_pool)).unwrap().await?; - let msg = send_response_poise( - ctx, + let _ = send_reply( + &ctx, CrackedMessage::Other(format!("Music channel set to {}", channel_id)), true, ) .await?; - data.add_msg_to_cache(guild_id, msg); + + Ok(()) +} + +use poise::serenity_prelude as serenity; + +#[poise::command(prefix_command, required_permissions = "ADMINISTRATOR")] +pub async fn music_denied_user( + ctx: Context<'_>, + #[description = "User to deny music commands to."] user: serenity::UserId, +) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + + let data = ctx.data(); + let _ = data.add_denied_music_user(guild_id, user).await; + + let opt_settings = data.guild_settings_map.read().await.clone(); + let settings = opt_settings.get(&guild_id); + + let pg_pool = ctx.data().database_pool.clone().unwrap(); + settings.map(|s| s.save(&pg_pool)).unwrap().await?; + + let _ = send_reply( + &ctx, + CrackedMessage::Other(format!("Denied user set to {}", user)), + true, + ) + .await?; Ok(()) } diff --git a/crack-core/src/commands/settings/set/set_premium.rs b/crack-core/src/commands/settings/set/set_premium.rs index 35de365d9..625a6fc7a 100644 --- a/crack-core/src/commands/settings/set/set_premium.rs +++ b/crack-core/src/commands/settings/set/set_premium.rs @@ -1,7 +1,7 @@ use crate::commands::CrackedError; use crate::db::GuildEntity; use crate::messaging::message::CrackedMessage; -use crate::utils::send_response_poise; +use crate::utils::send_reply; use crate::{Context, Error}; // /// Convenience type for readability. @@ -39,17 +39,22 @@ pub async fn set_premium_internal(ctx: Context<'_>, premium: bool) -> Result<(), GuildEntity::update_premium(&pool, guild_id.get() as i64, premium) .await .unwrap(); - let msg = send_response_poise(ctx, CrackedMessage::Premium(premium), true).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + let _ = send_reply(&ctx, CrackedMessage::Premium(premium), true).await?; Ok(()) } /// Set the premium status of the guild. -#[poise::command(prefix_command, owners_only)] +#[poise::command(category = "Settings", prefix_command, owners_only)] #[cfg(not(tarpaulin_include))] pub async fn premium( ctx: Context<'_>, #[description = "True or false setting for premium."] premium: bool, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, ) -> Result<(), Error> { + if help { + return crate::commands::help::wrapper(ctx).await; + } set_premium_internal(ctx, premium).await.map_err(Into::into) } diff --git a/crack-core/src/commands/settings/set/set_volume.rs b/crack-core/src/commands/settings/set/set_volume.rs index 187175e5a..df1a44a8b 100644 --- a/crack-core/src/commands/settings/set/set_volume.rs +++ b/crack-core/src/commands/settings/set/set_volume.rs @@ -25,12 +25,24 @@ pub async fn set_volume( /// Set the volume for this guild. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR" +)] pub async fn volume( ctx: Context<'_>, #[description = "Volume to set the bot settings to"] volume: f32, + #[flag] + #[description = "Show the help menu for this command."] + help: bool, ) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); + use crate::commands::CrackedError; + + if help { + return crate::commands::help::wrapper(ctx).await; + } + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; let (vol, old_vol) = { let guild_settings_map = &ctx.data().guild_settings_map; @@ -42,14 +54,14 @@ pub async fn volume( .await? .into_message() .await?; - ctx.data().add_msg_to_cache(guild_id, msg); + ctx.data().add_msg_to_cache(guild_id, msg).await; Ok(()) } #[cfg(test)] mod test { use crate::commands::settings::set::set_volume::set_volume; - use crate::guild::settings::{GuildSettingsMapParam, DEFAULT_VOLUME_LEVEL}; + use crate::guild::settings::GuildSettingsMapParam; use serenity::model::id::GuildId; #[tokio::test] @@ -57,9 +69,15 @@ mod test { let guild_id = GuildId::new(1); let guild_settings_map = GuildSettingsMapParam::default(); + // let init_volume = guild_settings_map + // .read() + // .await + // .get(&guild_id) + // .map(|x| x.volume) + // .unwrap_or(DEFAULT_VOLUME_LEVEL); let (vol, old_vol) = set_volume(&guild_settings_map, guild_id, 0.5).await; assert_eq!(vol, 0.5); - assert_eq!(old_vol, DEFAULT_VOLUME_LEVEL); + assert_eq!(old_vol, 0.5); assert_eq!( guild_settings_map .read() diff --git a/crack-core/src/commands/settings/set/set_welcome_settings.rs b/crack-core/src/commands/settings/set/set_welcome_settings.rs index 671bdfc88..0ac999abf 100644 --- a/crack-core/src/commands/settings/set/set_welcome_settings.rs +++ b/crack-core/src/commands/settings/set/set_welcome_settings.rs @@ -7,11 +7,16 @@ use crate::{ use serenity::all::{Channel, GuildId, Role}; /// Set password verification for the server. -#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES|MANAGE_ROLES" +)] #[cfg(not(tarpaulin_include))] pub async fn password_verify( ctx: Context<'_>, - #[description = "The channel use for verification message"] channel: Channel, + #[description = "The channel to use for verification message"] channel: Channel, #[description = "Password to verify"] password: String, #[description = "Role to add after successful verification"] auto_role: Role, #[rest] @@ -20,6 +25,7 @@ pub async fn password_verify( ) -> Result<(), Error> { let prefix = ctx.data().bot_settings.get_prefix(); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = get_guild_name(&ctx, guild_id).await; let welcome_settings = WelcomeSettings { channel_id: Some(channel.id().get()), message: Some(message.clone()), @@ -29,7 +35,7 @@ pub async fn password_verify( let msg = set_welcome_settings( ctx.data().clone(), guild_id, - get_guild_name(ctx.serenity_context(), guild_id), + guild_name, prefix.to_string(), welcome_settings, ) @@ -39,7 +45,12 @@ pub async fn password_verify( } /// Set the welcome settings for the server. -#[poise::command(prefix_command, ephemeral, required_permissions = "ADMINISTRATOR")] +#[poise::command( + category = "Settings", + prefix_command, + required_permissions = "ADMINISTRATOR", + required_bot_permissions = "SEND_MESSAGES|MANAGE_ROLES" +)] #[cfg(not(tarpaulin_include))] pub async fn welcome_settings( ctx: Context<'_>, @@ -50,6 +61,7 @@ pub async fn welcome_settings( ) -> Result<(), Error> { let prefix = ctx.data().bot_settings.get_prefix(); let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = get_guild_name(&ctx, guild_id).await; let welcome_settings = WelcomeSettings { channel_id: Some(channel.id().get()), message: Some(message.clone()), @@ -59,7 +71,7 @@ pub async fn welcome_settings( let msg = set_welcome_settings( ctx.data().clone(), guild_id, - get_guild_name(ctx.serenity_context(), guild_id), + guild_name, prefix.to_string(), welcome_settings, ) diff --git a/crack-core/src/commands/settings/toggle/self_deafen.rs b/crack-core/src/commands/settings/toggle/self_deafen.rs index 77a7573a8..9e475216c 100644 --- a/crack-core/src/commands/settings/toggle/self_deafen.rs +++ b/crack-core/src/commands/settings/toggle/self_deafen.rs @@ -1,5 +1,5 @@ use crate::{ - errors::CrackedError, guild::settings::GuildSettings, utils::get_guild_name, Context, Data, + errors::CrackedError, guild::settings::GuildSettings, http_utils::CacheHttpExt, Context, Data, Error, }; use serenity::all::GuildId; @@ -9,14 +9,16 @@ use sqlx::PgPool; #[poise::command(prefix_command, owners_only, ephemeral)] #[cfg(not(tarpaulin_include))] pub async fn self_deafen(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = ctx.guild_name_from_guild_id(guild_id).await?; let res = toggle_self_deafen( ctx.data().clone(), ctx.data() .database_pool .clone() .ok_or(CrackedError::NoDatabasePool)?, - ctx.guild_id().ok_or(CrackedError::NoGuildId)?, - get_guild_name(ctx.serenity_context(), ctx.guild_id().unwrap()), + guild_id, + Some(guild_name), ctx.data().bot_settings.get_prefix(), ) .await?; diff --git a/crack-core/src/commands/settings/toggle/toggle_autopause.rs b/crack-core/src/commands/settings/toggle/toggle_autopause.rs index 84ca24bad..7c933bb69 100644 --- a/crack-core/src/commands/settings/toggle/toggle_autopause.rs +++ b/crack-core/src/commands/settings/toggle/toggle_autopause.rs @@ -1,7 +1,5 @@ -use crate::{ - errors::CrackedError, guild::settings::GuildSettings, utils::get_guild_name, Context, Data, - Error, -}; +use crate::http_utils::CacheHttpExt; +use crate::{errors::CrackedError, guild::settings::GuildSettings, Context, Data, Error}; use serenity::all::GuildId; use sqlx::PgPool; @@ -13,14 +11,16 @@ use sqlx::PgPool; )] #[cfg(not(tarpaulin_include))] pub async fn toggle_autopause(ctx: Context<'_>) -> Result<(), Error> { - let res = toggle_autopause_( + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let guild_name = ctx.guild_name_from_guild_id(guild_id).await?; + let res = toggle_autopause_internal( ctx.data().clone(), ctx.data() .database_pool .clone() .ok_or(CrackedError::NoDatabasePool)?, - ctx.guild_id().ok_or(CrackedError::NoGuildId)?, - get_guild_name(ctx.serenity_context(), ctx.guild_id().unwrap()), + guild_id, + Some(guild_name), ctx.data().bot_settings.get_prefix(), ) .await?; @@ -32,7 +32,7 @@ pub async fn toggle_autopause(ctx: Context<'_>) -> Result<(), Error> { /// Toggle the autopause for the bot. #[cfg(not(tarpaulin_include))] -pub async fn toggle_autopause_( +pub async fn toggle_autopause_internal( data: Data, pool: PgPool, guild_id: GuildId, diff --git a/crack-core/src/commands/music/clean.rs b/crack-core/src/commands/utility/clean.rs similarity index 74% rename from crack-core/src/commands/music/clean.rs rename to crack-core/src/commands/utility/clean.rs index 42cb09acf..298180b52 100644 --- a/crack-core/src/commands/music/clean.rs +++ b/crack-core/src/commands/utility/clean.rs @@ -1,17 +1,30 @@ use crate::{ - errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, - Error, + commands::sub_help as help, errors::CrackedError, messaging::message::CrackedMessage, + utils::send_reply, Context, Error, }; const CHAT_CLEANUP_SECONDS: u64 = 15; // 60 * 60 * 24 * 7; /// Clean up old messages from the bot. #[cfg(not(tarpaulin_include))] -#[poise::command(prefix_command, slash_command, guild_only)] +#[poise::command( + category = "Utility", + prefix_command, + slash_command, + guild_only, + required_permissions = "MANAGE_MESSAGES", + required_bot_permissions = "MANAGE_MESSAGES", + subcommands("help") +)] pub async fn clean(ctx: Context<'_>) -> Result<(), Error> { + clean_internal(ctx).await +} + +/// Clean up old messages from the bot, internal fucntion. +pub async fn clean_internal(ctx: Context<'_>) -> Result<(), Error> { let guild_id = ctx.guild_id().unwrap(); let time_ordered_messages = { - let mut message_cache = ctx.data().guild_msg_cache_ordered.lock().unwrap(); + let mut message_cache = ctx.data().guild_msg_cache_ordered.lock().await; &mut message_cache .get_mut(&guild_id) .ok_or(CrackedError::Other("No messages in cache"))? @@ -48,6 +61,6 @@ pub async fn clean(ctx: Context<'_>) -> Result<(), Error> { } status_msg.delete(&ctx.serenity_context()).await?; - send_response_poise(ctx, CrackedMessage::Clean(deleted), true).await?; + send_reply(&ctx, CrackedMessage::Clean(deleted), true).await?; Ok(()) } diff --git a/crack-core/src/commands/utility/debug.rs b/crack-core/src/commands/utility/debug.rs new file mode 100644 index 000000000..43f62cb13 --- /dev/null +++ b/crack-core/src/commands/utility/debug.rs @@ -0,0 +1,117 @@ +use crate::commands::{uptime_internal, CrackedError}; +use crate::messaging::message::CrackedMessage; +use crate::poise_ext::{ContextExt, MessageInterfaceCtxExt}; +use crate::{Context, Error}; +use chrono::Duration; +use poise::serenity_prelude as serenity; +use serenity::ChannelId; +use serenity::Mentionable; +use songbird::tracks::PlayMode; +use std::borrow::Cow; +use std::fmt; + +/// The status of the bot. +#[derive(Debug, Clone, Default)] +pub struct BotStatus<'ctx> { + pub name: Cow<'ctx, String>, + pub play_mode: PlayMode, + pub queue_len: usize, + pub current_channel: Option, + pub uptime: Duration, + pub calling_user: String, +} + +impl<'ctx> fmt::Display for BotStatus<'ctx> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + r#" + BotStatus {{ + name: {}, + play_mode: {:?}, + queue_len: {}, + current_channel: {:?}, + uptime: {:?} + }}"#, + self.name, self.play_mode, self.queue_len, self.current_channel, self.uptime + ) + } +} + +#[poise::command( + category = "Utility", + slash_command, + prefix_command, + guild_only, + owners_only +)] +#[cfg(not(tarpaulin_include))] +pub async fn debug(ctx: Context<'_>) -> Result<(), Error> { + debug_internal(ctx).await +} + +#[cfg(not(tarpaulin_include))] +pub async fn debug_internal(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::GuildOnly)?; + let manager = songbird::get(ctx.serenity_context()).await.unwrap(); + let _guild = ctx.guild().ok_or(CrackedError::NoGuildCached)?.clone(); + let user_id = ctx.get_user_id(); + let mention_caller = user_id.mention().to_string(); + let bot_name = ctx.cache().current_user().mention().to_string(); + + // Get the voice channel we're in if any. + let call = manager.get(guild_id); + let mut vc_status = match call { + Some(call) => { + let handler = call.lock().await; + let channel_id = handler.current_channel(); + let _is_connected = handler.current_connection().is_some(); + let queue = handler.queue(); + let track = queue.current().clone(); + + match track { + Some(track) => BotStatus { + play_mode: track.get_info().await.unwrap_or_default().playing, + current_channel: channel_id.map(|id| serenity::ChannelId::new(id.0.into())), + queue_len: queue.clone().len(), + ..Default::default() + }, + None => Default::default(), + } + }, + _ => Default::default(), + }; + let uptime = match uptime_internal(ctx).await { + CrackedMessage::Uptime { seconds, .. } => Duration::seconds(seconds as i64), + _ => Duration::zero(), + }; + vc_status.uptime = uptime; + vc_status.name = Cow::Owned(bot_name); + vc_status.calling_user = mention_caller; + + let msg = vc_status.to_string(); + ctx.send_reply(CrackedMessage::Other(msg), true).await?; + + Ok(()) +} + +#[cfg(test)] +mod test { + use super::*; + + #[tokio::test] + async fn test_print_bot_status() { + let bot_status = BotStatus { + name: Cow::Owned("bot".to_string()), + play_mode: PlayMode::Play, + queue_len: 1, + current_channel: Some(ChannelId::new(1)), + uptime: Duration::seconds(1), + calling_user: "user".to_string(), + }; + + let msg = bot_status.to_string(); + + assert!(msg.contains("bot")); + } +} diff --git a/crack-core/src/commands/utility/invite.rs b/crack-core/src/commands/utility/invite.rs new file mode 100644 index 000000000..1070f57b9 --- /dev/null +++ b/crack-core/src/commands/utility/invite.rs @@ -0,0 +1,15 @@ +use crate::{poise_ext::ContextExt, Context, Error}; + +/// Invite link for the bot +#[cfg(not(tarpaulin_include))] +#[poise::command(category = "Utility", slash_command, prefix_command)] +pub async fn invite(ctx: Context<'_>) -> Result<(), Error> { + invite_internal(ctx).await +} + +/// Testable internal function for invite. +#[cfg(not(tarpaulin_include))] +pub async fn invite_internal(ctx: Context<'_>) -> Result<(), Error> { + let _ = ctx.send_invite_link().await?; + Ok(()) +} diff --git a/crack-core/src/commands/utility/mod.rs b/crack-core/src/commands/utility/mod.rs new file mode 100644 index 000000000..73d4c9d00 --- /dev/null +++ b/crack-core/src/commands/utility/mod.rs @@ -0,0 +1,78 @@ +pub mod clean; +mod debug; +pub mod invite; +pub mod ping; +mod say; +mod smoketest; +pub mod version; + +pub use clean::*; +pub use debug::*; +pub use invite::*; +pub use ping::*; +pub use say::*; +pub use smoketest::*; +pub use version::*; + +use crate::{CommandResult, Context, CrackedMessage, Error}; +use poise::serenity_prelude::Mentionable; + +/// Get information about the servers this bot is in. +#[cfg(not(tarpaulin_include))] +#[poise::command(category = "Utility", slash_command, prefix_command, owners_only)] +pub async fn servers(ctx: Context<'_>) -> Result<(), Error> { + poise::builtins::servers(ctx).await?; + Ok(()) +} + +/// Shows how long TTS Bot has been online +#[poise::command( + category = "Utility", + prefix_command, + slash_command, + required_bot_permissions = "SEND_MESSAGES" +)] +pub async fn uptime(ctx: Context<'_>) -> CommandResult { + let now = std::time::SystemTime::now(); + let seconds = now.duration_since(ctx.data().start_time)?.as_secs(); + let mention = { + let current_user = ctx.cache().current_user(); + current_user.mention().to_string() + }; + + let msg = CrackedMessage::Uptime { mention, seconds }; + + crate::utils::send_reply(&ctx, msg, true).await?; + + Ok(()) +} + +pub async fn uptime_internal(ctx: Context<'_>) -> CrackedMessage { + let now = std::time::SystemTime::now(); + let seconds = now + .duration_since(ctx.data().start_time) + .unwrap_or_default() + .as_secs(); + let mention = { + let current_user = ctx.cache().current_user(); + current_user.mention().to_string() + }; + + Into::into(CrackedMessage::Uptime { mention, seconds }) +} + +/// Get all the utility commands. +pub fn utility_commands() -> [crate::Command; 10] { + [ + clean(), + debug(), + invite(), + ping(), + servers(), + saychan(), + saychanid(), + smoketest(), + uptime(), + version(), + ] +} diff --git a/crack-core/src/commands/utility/ping.rs b/crack-core/src/commands/utility/ping.rs new file mode 100644 index 000000000..0d7daff50 --- /dev/null +++ b/crack-core/src/commands/utility/ping.rs @@ -0,0 +1,28 @@ +use poise::CreateReply; +use serenity::all::{Color, CreateEmbed}; + +use crate::messaging::message::CrackedMessage; +use crate::utils::send_reply_embed; +use crate::{Context, Error}; + +/// Ping the bot +#[cfg(not(tarpaulin_include))] +#[poise::command(category = "Utility", slash_command, prefix_command)] +pub async fn ping(ctx: Context<'_>) -> Result<(), Error> { + ping_internal(ctx).await +} + +/// Ping the bot internal function +#[cfg(not(tarpaulin_include))] +pub async fn ping_internal(ctx: Context<'_>) -> Result<(), Error> { + let start = std::time::Instant::now(); + let msg = send_reply_embed(&ctx, CrackedMessage::Pong).await?; + let end = std::time::Instant::now(); + let msg_str = format!("Pong! ({}ms)", (end - start).as_millis()); + let edited = CreateReply::default().embed( + CreateEmbed::default() + .description(msg_str) + .color(Color::from(CrackedMessage::Pong)), + ); + msg.edit(ctx, edited).await.map_err(Error::from) +} diff --git a/crack-core/src/commands/utility/say.rs b/crack-core/src/commands/utility/say.rs new file mode 100644 index 000000000..9cf1fa41a --- /dev/null +++ b/crack-core/src/commands/utility/say.rs @@ -0,0 +1,55 @@ +use crate::commands::help; +use crate::{Context, Error}; +use serenity::all::{Channel, ChannelId}; + +/// Have the bot say something in a channel. +#[cfg(not(tarpaulin_include))] +#[poise::command( + category = "Utility", + slash_command, + prefix_command, + owners_only, + required_permissions = "ADMINISTRATOR" +)] +pub async fn saychan( + ctx: Context<'_>, + #[flag] + #[description = "show the help menu for this command."] + help: bool, + #[description = "Channel to send the message to"] chan: Channel, + #[description = "Message to send"] msg: String, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + say_internal(ctx, chan.id(), msg).await +} + +/// Have the bot say something in a channel, by id. +#[cfg(not(tarpaulin_include))] +#[poise::command( + category = "Utility", + slash_command, + prefix_command, + owners_only, + required_permissions = "ADMINISTRATOR" +)] +pub async fn saychanid( + ctx: Context<'_>, + #[flag] + #[description = "show the help menu for this command."] + help: bool, + #[description = "Channel ID of channel to send message to"] chan: ChannelId, + #[description = "Message to send"] msg: String, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + say_internal(ctx, chan, msg).await +} + +/// Internal say function. +pub async fn say_internal(ctx: Context<'_>, chan_id: ChannelId, msg: String) -> Result<(), Error> { + chan_id.say(&ctx, msg).await?; + Ok(()) +} diff --git a/crack-core/src/commands/utility/smoketest.rs b/crack-core/src/commands/utility/smoketest.rs new file mode 100644 index 000000000..dc6580294 --- /dev/null +++ b/crack-core/src/commands/utility/smoketest.rs @@ -0,0 +1,137 @@ +use crate::commands::help; +use crate::CrackedResult; +use crate::{Context, Error}; +use serenity::all::{ChannelId, GuildId}; + +/// Struct that defines a smoke test to run. +#[derive(Debug, Clone)] +pub struct SmokeTest<'a> { + ctx: Context<'a>, + chan: ChannelId, + say_msg: String, + wait_secs: Option, + want_response: Option, +} + +/// Implemention of the SmokeTest struct. +impl<'a> SmokeTest<'a> { + pub fn new(ctx: Context<'a>, chan: ChannelId, say_msg: String) -> Self { + Self { + ctx, + chan, + say_msg, + wait_secs: Some(2), + want_response: None, + } + } + + pub fn new_generator(ctx: Context<'a>, chan: ChannelId) -> impl Fn(String) -> Self { + move |say_msg| SmokeTest { + ctx, + chan, + say_msg, + wait_secs: Some(2), + want_response: None, + } + } + + pub fn with_wait_secs(mut self, wait_secs: u64) -> Self { + self.wait_secs = Some(wait_secs); + self + } + + pub fn with_want_response(mut self, want_response: String) -> Self { + self.want_response = Some(want_response); + self + } + + pub async fn run(&self) -> CrackedResult<()> { + run_smoke_test(self.clone()).await + } +} + +/// Have the bot say something in a channel. +#[cfg(not(tarpaulin_include))] +#[poise::command( + category = "Testing", + slash_command, + prefix_command, + owners_only, + required_permissions = "ADMINISTRATOR" +)] +pub async fn smoketest( + ctx: Context<'_>, + #[flag] + #[description = "show the help menu for this command."] + help: bool, +) -> Result<(), Error> { + if help { + return help::wrapper(ctx).await; + } + + smoketest_internal(ctx).await +} + +/// Run the smoke tests. +pub async fn smoketest_internal(ctx: Context<'_>) -> Result<(), Error> { + let beg = std::time::SystemTime::now(); + + let test_chan = ChannelId::new(1232025110802862180); + let _test_guild = GuildId::new(1220832110210846800); + + // Send message to testing channel to trigger the testee bot to respond + let tests = get_all_test_messages(); + let test_gen = SmokeTest::new_generator(ctx, test_chan); + for test_msg in tests { + let test = test_gen(test_msg); + test.run().await?; + } + + let end = std::time::SystemTime::now(); + let delta = end.duration_since(beg)?.as_secs(); + + tracing::info!("Smoke test took {} seconds", delta); + + Ok(()) +} + +/// Get all the test messages to send for the smoke tests. +pub fn get_all_test_messages() -> Vec { + vec![ + "Beginning Some Test...", + "{test}!invite", + "{test}!ping", + "{test}!version", + "{test}!servers", + "{test}!uptime", + "{test}!clean", + "{test}!vote", + // Settings + "{test}!settings", + "{test}!settings get", + "{test}!settings get auto_role", + "{test}!settings get idle_timeouit", + "{test}!settings get premium", + "{test}!settings get volume", + "{test}!settings get welcome_settings", + "{test}!settings get log_channels", + "{test}!say_channel <#1232025110802862180> Smoke Test...", + "{test}!say_channel_id 1232025110802862180 Complete.", + ] + .into_iter() + .map(ToString::to_string) + .collect() +} + +/// Run a smoke test. +pub async fn run_smoke_test(test: SmokeTest<'_>) -> CrackedResult<()> { + test.chan.say(&test.ctx, test.say_msg).await?; + if let Some(wait_secs) = test.wait_secs { + tokio::time::sleep(tokio::time::Duration::from_secs(wait_secs)).await; + } + if let Some(want_response) = &test.want_response { + // let response = test.ctx.await?; + tracing::info!("Want response: {:?}", want_response); + } + Ok(()) +} diff --git a/crack-core/src/commands/utility/version.rs b/crack-core/src/commands/utility/version.rs new file mode 100644 index 000000000..cbdb220d4 --- /dev/null +++ b/crack-core/src/commands/utility/version.rs @@ -0,0 +1,31 @@ +use crate::guild::operations::GuildSettingsOperations; +use crate::{ + messaging::{message::CrackedMessage, messages::UNKNOWN}, + utils::send_reply, + Context, CrackedError, Error, +}; + +/// Get the build version of this bot. +#[cfg(not(tarpaulin_include))] +#[poise::command(category = "Utility", slash_command, prefix_command)] +pub async fn version(ctx: Context<'_>) -> Result<(), Error> { + version_internal(ctx).await +} + +/// Get the build version of this bot, internal function. +pub async fn version_internal(ctx: Context<'_>) -> Result<(), Error> { + let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; + let reply_with_embed = ctx.data().get_reply_with_embed(guild_id).await; + let current = option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| UNKNOWN); + let hash = option_env!("VERGEN_GIT_SHA").unwrap_or_else(|| UNKNOWN); + let _ = send_reply( + &ctx, + CrackedMessage::Version { + current: current.to_owned(), + hash: hash.to_owned(), + }, + reply_with_embed, + ) + .await?; + Ok(()) +} diff --git a/crack-core/src/commands/version.rs b/crack-core/src/commands/version.rs deleted file mode 100644 index 7d5053dd1..000000000 --- a/crack-core/src/commands/version.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::guild::operations::GuildSettingsOperations; -use crate::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; - -/// Get the current version of the bot. -#[cfg(not(tarpaulin_include))] -#[poise::command(slash_command, prefix_command)] -pub async fn version(ctx: Context<'_>) -> Result<(), Error> { - let guild_id = ctx.guild_id().unwrap(); - let reply_with_embed = ctx.data().get_reply_with_embed(guild_id).await; - let current = option_env!("CARGO_PKG_VERSION").unwrap_or_else(|| "Unknown"); - let hash = option_env!("GIT_HASH").unwrap_or_else(|| "Unknown"); - let msg = send_response_poise( - ctx, - CrackedMessage::Version { - current: current.to_owned(), - hash: hash.to_owned(), - }, - reply_with_embed, - ) - .await?; - ctx.data().add_msg_to_cache(guild_id, msg); - Ok(()) -} diff --git a/crack-core/src/config.rs b/crack-core/src/config.rs new file mode 100644 index 000000000..c286accd9 --- /dev/null +++ b/crack-core/src/config.rs @@ -0,0 +1,357 @@ +#[cfg(feature = "crack-metrics")] +use crate::metrics::COMMAND_ERRORS; +use crate::poise_ext::PoiseContextExt; +use crate::{ + db, + errors::CrackedError, + guild::settings::{GuildSettings, GuildSettingsMap}, + handlers::{handle_event, SerenityHandler}, + http_utils::CacheHttpExt, + http_utils::SendMessageParams, + messaging::message::CrackedMessage, + utils::{check_reply, count_command}, + BotConfig, Data, DataInner, Error, EventLogAsync, PhoneCodeData, +}; +use colored::Colorize; +use poise::serenity_prelude::{Client, FullEvent, GatewayIntents, GuildId, UserId}; +use songbird::serenity::SerenityInit; +use std::{collections::HashMap, process::exit, sync::Arc, time::Duration}; +use tokio::sync::RwLock; + +/// on_error is called when an error occurs in the framework. +async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { + // This is our custom error handler + // They are many errors that can occur, so we only handle the ones we want to customize + // and forward the rest to the default handler + match error { + poise::FrameworkError::Setup { error, .. } => panic!("Failed to start bot: {:?}", error), + poise::FrameworkError::EventHandler { error, event, .. } => match event { + FullEvent::PresenceUpdate { .. } => { /* Ignore PresenceUpdate in terminal logging, too spammy */ + }, + _ => { + tracing::warn!( + "{} {} {} {}", + "In event handler for ".yellow(), + event.snake_case_name().yellow().italic(), + " event: ".yellow(), + error.to_string().yellow().bold(), + ); + }, + }, + poise::FrameworkError::Command { error, ctx, .. } => { + let myerr = CrackedError::Poise(error); + let params = SendMessageParams::new(CrackedMessage::CrackedError(myerr)); + check_reply(ctx.send_message(params).await.map_err(Into::into)); + #[cfg(feature = "crack-metrics")] + COMMAND_ERRORS + .with_label_values(&[&ctx.command().qualified_name]) + .inc(); + }, + error => { + if let Err(e) = poise::builtins::on_error(error).await { + tracing::error!("Error while handling error: {}", e) + } + }, + } +} + +/// Create the poise framework from the bot config. +pub async fn poise_framework( + config: BotConfig, + event_log_async: EventLogAsync, +) -> Result { + // FrameworkOptions contains all of poise's configuration option in one struct + // Every option can be omitted to use its default value + + tracing::warn!("Using prefix: {}", config.get_prefix()); + let up_prefix = config.get_prefix().to_ascii_uppercase(); + // FIXME: Is this the proper way to allocate this memory? + let up_prefix_cloned = Box::leak(Box::new(up_prefix.clone())); + + let commands = crate::commands::all_commands(); + let _commands_map = crate::commands::all_commands_map(); + let commands_str = crate::commands::all_command_names(); + + tracing::warn!("Commands: {:#?}", commands_str); + + let options = poise::FrameworkOptions::<_, Error> { + commands, + owners: config + .owners + .as_ref() + .unwrap_or(&vec![]) + .iter() + .map(|id| UserId::new(*id)) + .collect(), + prefix_options: poise::PrefixFrameworkOptions { + prefix: Some(config.get_prefix()), + ignore_bots: false, // This is for automated smoke tests + edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600)).into()), + additional_prefixes: vec![poise::Prefix::Literal(up_prefix_cloned)], + stripped_dynamic_prefix: Some(|ctx, msg, data| { + Box::pin(async move { + // allow specific bots with specific prefixes to use bot commands for testing. + if msg.author.bot { + if !crate::poise_ext::check_bot_message(ctx, msg) { + return Ok(None); + } + + // FIXME: Make this less hacky + if !msg.content.starts_with("{test}!") + || ctx.cache.current_user().name.starts_with("Crack") + { + return Ok(None); + } else { + return Ok(Some(msg.content.split_at(7))); + } + } + let guild_id = match msg.guild_id { + Some(id) => id, + None => { + tracing::warn!("No guild id found"); + GuildId::new(1) + }, + }; + let guild_settings_map = data.guild_settings_map.read().await.clone(); + + if let Some(guild_settings) = guild_settings_map.get(&guild_id) { + let prefixes = &guild_settings.additional_prefixes; + if prefixes.is_empty() { + tracing::trace!( + "Prefix is empty for guild {}", + guild_settings.guild_name + ); + return Ok(None); + } + + if let Some(prefix_len) = check_prefixes(prefixes, &msg.content) { + Ok(Some(msg.content.split_at(prefix_len))) + } else { + tracing::trace!("Prefix not found"); + Ok(None) + } + } else { + tracing::warn!("Guild not found in guild settings map"); + Ok(None) + } + }) + }), + ..Default::default() + }, + // The global error handler for all error cases that may occur + on_error: |error| Box::pin(on_error(error)), + // This code is run before every command + pre_command: |ctx| { + Box::pin(async move { + tracing::info!(">>> {}...", ctx.command().qualified_name); + count_command(ctx.command().qualified_name.as_ref(), ctx.is_prefix()); + }) + }, + // This code is run after a command if it was successful (returned Ok) + post_command: |ctx| { + Box::pin(async move { + tracing::info!("<<< {}!", ctx.command().qualified_name); + }) + }, + // Every command invocation must pass this check to continue execution + command_check: Some(|ctx| { + Box::pin(async move { + let guild_id = ctx.guild_id(); + let name = match guild_id { + None => return Ok(true), + Some(guild_id) => ctx.guild_name_from_guild_id(guild_id).await, + } + .unwrap_or("Unknown".to_string()); + tracing::warn!("Guild: {}", name); + Ok(true) + }) + }), + event_handler: |ctx, event, framework, data_global| { + Box::pin(async move { handle_event(ctx, event, framework, data_global).await }) + }, + // Enforce command checks even for owners (enforced by default) + // Set to true to bypass checks, which is useful for testing + skip_checks_for_owners: false, + initialize_owners: false, + ..Default::default() + }; + let config_ref = &config; + let guild_settings_map = config_ref + .guild_settings_map + .as_ref() + .map(|x| { + x.iter() + .map(|gs| (gs.guild_id, gs.clone())) + .collect::>() + }) + .unwrap_or_default(); + + let db_url: &str = &config_ref.get_database_url(); + let database_pool = match sqlx::postgres::PgPoolOptions::new().connect(db_url).await { + Ok(pool) => Some(pool), + Err(e) => { + tracing::error!("Error getting database pool: {}, db_url: {}", e, db_url); + None + }, + }; + let db_channel = match database_pool.clone().map(db::worker_pool::setup_workers) { + Some(c) => Some(c.await), + None => None, + }; + // let rt = tokio::runtime::Builder::new_multi_thread() + // .enable_all() + // .build() + // .unwrap(); + // let handle = rt.handle(); + let cloned_map = guild_settings_map.clone(); + let data = Data(Arc::new(DataInner { + phone_data: PhoneCodeData::load().unwrap(), + bot_settings: config.clone(), + guild_settings_map: Arc::new(RwLock::new(cloned_map)), + event_log_async, + database_pool, + db_channel, + ..Default::default() + })); + + let intents = GatewayIntents::non_privileged() + | GatewayIntents::privileged() + | GatewayIntents::GUILDS + | GatewayIntents::GUILD_MEMBERS + | GatewayIntents::GUILD_MODERATION + | GatewayIntents::GUILD_EMOJIS_AND_STICKERS + | GatewayIntents::GUILD_INTEGRATIONS + | GatewayIntents::GUILD_WEBHOOKS + | GatewayIntents::GUILD_INVITES + | GatewayIntents::GUILD_VOICE_STATES + | GatewayIntents::GUILD_PRESENCES + | GatewayIntents::GUILD_MESSAGES + | GatewayIntents::GUILD_MESSAGE_TYPING + | GatewayIntents::GUILD_MESSAGE_REACTIONS + | GatewayIntents::DIRECT_MESSAGES + | GatewayIntents::DIRECT_MESSAGE_TYPING + | GatewayIntents::DIRECT_MESSAGE_REACTIONS + | GatewayIntents::GUILD_SCHEDULED_EVENTS + | GatewayIntents::AUTO_MODERATION_CONFIGURATION + | GatewayIntents::AUTO_MODERATION_EXECUTION + | GatewayIntents::MESSAGE_CONTENT; + + let token = config + .credentials + .expect("Error getting discord token") + .discord_token; + let data2 = data.clone(); + // FIXME: Why can't we use framework.user_data() later in this function? (it hangs) + let framework = poise::Framework::new(options, |ctx, ready, framework| { + Box::pin(async move { + tracing::info!("Logged in as {}", ready.user.name); + crate::commands::register::register_globally_cracked( + &ctx, + &framework.options().commands, + ) + .await?; + ctx.data + .write() + .await + .insert::(guild_settings_map.clone()); + Ok(data.clone()) + }) + }); + let serenity_handler = SerenityHandler { + is_loop_running: false.into(), + data: data2.clone(), + }; + + // let bot_test_handler = Arc::new(ForwardBotTestCommandsHandler { + + // options: Default::default(), + // cmd_lookup: commands_map, + // shard_manager: std::sync::Mutex::new(None), + // }); + let client = Client::builder(token, intents) + .framework(framework) + .register_songbird() + .event_handler(serenity_handler) + //.event_handler_arc(bot_test_handler.clone()) + .await + .unwrap(); + //*bot_test_handler.shard_manager.lock().unwrap() = Some(client.shard_manager.clone()); + let shard_manager = client.shard_manager.clone(); + + // let data2 = client.data.clone(); + tokio::spawn(async move { + #[cfg(unix)] + { + use tokio::signal::unix as signal; + + let [mut s1, mut s2, mut s3] = [ + signal::signal(signal::SignalKind::hangup()).unwrap(), + signal::signal(signal::SignalKind::interrupt()).unwrap(), + signal::signal(signal::SignalKind::terminate()).unwrap(), + ]; + + tokio::select!( + v = s1.recv() => v.unwrap(), + v = s2.recv() => v.unwrap(), + v = s3.recv() => v.unwrap(), + ); + } + #[cfg(windows)] + { + let (mut s1, mut s2) = ( + tokio::signal::windows::ctrl_c().unwrap(), + tokio::signal::windows::ctrl_break().unwrap(), + ); + + tokio::select!( + v = s1.recv() => v.unwrap(), + v = s2.recv() => v.unwrap(), + ); + } + + tracing::warn!("Received Ctrl-C, shutting down..."); + let guilds = data2.guild_settings_map.read().await.clone(); + let pool = data2.clone().database_pool.clone(); + + if pool.is_some() { + let p = pool.unwrap(); + for (k, v) in guilds { + tracing::warn!("Saving Guild: {}", k); + match v.save(&p).await { + Ok(_) => {}, + Err(e) => { + tracing::error!("Error saving guild settings: {}", e); + }, + } + } + } + + shard_manager.clone().shutdown_all().await; + + exit(0); + }); + + // let shard_manager_2 = client.shard_manager.clone(); + // tokio::spawn(async move { + // loop { + // let count = shard_manager_2.shards_instantiated().await.len(); + // let intents = shard_manager_2.intents(); + + // tracing::warn!("Shards: {}, Intents: {:?}", count, intents); + + // tokio::time::sleep(Duration::from_secs(10)).await; + // } + // }); + + Ok(client) +} + +/// Checks if the message starts with any of the given prefixes. +fn check_prefixes(prefixes: &[String], content: &str) -> Option { + for prefix in prefixes { + if content.starts_with(prefix) { + return Some(prefix.len()); + } + } + None +} diff --git a/crack-core/src/connection.rs b/crack-core/src/connection.rs index cfb3d5e4a..20be4ea8b 100644 --- a/crack-core/src/connection.rs +++ b/crack-core/src/connection.rs @@ -5,6 +5,8 @@ use self::serenity::model::{ use crate::{errors::CrackedError, Error}; use poise::serenity_prelude as serenity; +/// Enum for types of voice connection relationships. +#[derive(Debug, PartialEq)] pub enum Connection { User(ChannelId), Bot(ChannelId), @@ -13,6 +15,7 @@ pub enum Connection { Neither, } +/// Check the voice connection relationship to anopther user_id (bot). pub fn check_voice_connections(guild: &Guild, user_id: &UserId, bot_id: &UserId) -> Connection { let user_channel = get_voice_channel_for_user(guild, user_id).ok(); let bot_channel = get_voice_channel_for_user(guild, bot_id).ok(); @@ -32,6 +35,7 @@ pub fn check_voice_connections(guild: &Guild, user_id: &UserId, bot_id: &UserId) } } +/// Get the voice channel a user is in within a guild. pub fn get_voice_channel_for_user(guild: &Guild, user_id: &UserId) -> Result { guild .voice_states @@ -39,3 +43,42 @@ pub fn get_voice_channel_for_user(guild: &Guild, user_id: &UserId) -> Result Result { + match get_voice_channel_for_user(guild, user_id) { + Ok(channel_id) => Ok(channel_id), + Err(_) => { + tracing::warn!( + "User {} is not in a voice channel in guild {}", + user_id, + guild.id + ); + Err(CrackedError::WrongVoiceChannel.into()) + }, + } +} + +#[cfg(test)] +mod test { + use super::{check_voice_connections, Connection}; + use poise::serenity_prelude as serenity; + use serenity::Guild; + use serenity::UserId; + + #[test] + fn test_check_voice_connections() { + let guild = Guild::default(); + let user_id = UserId::new(1); + let bot_id = UserId::new(2); + + assert_eq!( + check_voice_connections(&guild, &user_id, &bot_id), + Connection::Neither + ); + } +} diff --git a/crack-core/src/db/guild.rs b/crack-core/src/db/guild.rs index b1725aaae..b056ad314 100644 --- a/crack-core/src/db/guild.rs +++ b/crack-core/src/db/guild.rs @@ -1,13 +1,21 @@ +use crate::{ + errors::CrackedError, + guild::{ + permissions::{GenericPermissionSettings, GenericPermissionSettingsReadWCommand}, + settings::{GuildSettings, WelcomeSettings}, + }, + CrackedResult, Error as SerenityError, +}; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; -use serenity::all::GuildId; use sqlx::PgPool; +use std::collections::HashMap; -use crate::{ - errors::CrackedError, - guild::settings::{GuildSettings, WelcomeSettings}, - Error as SerenityError, -}; +pub struct GuildPermissionPivot { + pub guild_id: i64, + pub permission_id: i64, + pub kind: i32, +} #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct GuildSettingsRead { @@ -259,15 +267,26 @@ impl GuildEntity { .save(pool, guild_id) .await?; } - if settings.command_channels.music_channel.is_some() { - settings - .command_channels - .music_channel - .as_ref() - .unwrap() - .save(pool) - .await?; - } + // let guard: RwLockReadGuard = settings.command_channels.read().await; + // for (cmd, val) in guard.clone() { + // //.expect("BUG! SHOULD BE SET") { + // for (channel_id, perms) in val { + // let perms_borrow = if perms.id == 0 { + // let perms = perms.insert_permission_settings(pool).await?; + // perms.clone() + // } else { + // perms + // }; + // let ch = CommandChannel { + // command: cmd.to_string(), + // channel_id: serenity::ChannelId::new(channel_id), + // guild_id: GuildId::new(guild_id as u64), + // permission_settings: perms_borrow.clone(), + // }; + // CommandChannel::save(&ch, pool).await?; + // } + // } + Ok(()) } @@ -340,13 +359,12 @@ impl GuildEntity { }?; let welcome_settings = GuildEntity::get_welcome_settings(pool, self.id).await?; let log_settings = GuildEntity::get_log_settings(pool, self.id).await?; - let command_channels = - crate::guild::settings::CommandChannels::load(GuildId::new(self.id as u64), pool) - .await?; + let cmd_settings = GuildEntity::load_command_settings(self.id, pool).await?; + Ok(GuildSettings::from(settings) .with_welcome_settings(welcome_settings) .with_log_settings(log_settings) - .with_command_channels(command_channels)) + .with_command_settings(cmd_settings)) } /// Create a new guild entity struct, which can be used to interact with the database. @@ -435,15 +453,13 @@ impl GuildEntity { .fetch_one(pool) .await?; - let guild_id_2 = GuildId::new(guild_id as u64); let welcome_settings = GuildEntity::get_welcome_settings(pool, guild_id).await?; let log_settings = GuildEntity::get_log_settings(pool, guild_id).await?; - let command_channels = - crate::guild::settings::CommandChannels::load(guild_id_2, pool).await?; + let command_settings = GuildEntity::load_command_settings(guild_id, pool).await?; let guild_settings = GuildSettings::from(guild_settings) .with_welcome_settings(welcome_settings) .with_log_settings(log_settings) - .with_command_channels(command_channels); + .with_command_settings(command_settings); (guild_entity, guild_settings) }, }; @@ -484,137 +500,27 @@ impl GuildEntity { .map(|_| ()) .map_err(|e| e.into()) } -} -// #[async_trait] -// pub trait ConnectionPool: Sync + Send { -// async fn connect(&self, url: &str) -> Result; -// fn to_pg_pool(&self) -> PgPool; -// } - -// #[async_trait] -// impl ConnectionPool for PgPool { -// async fn connect(&self, url: &str) -> Result { -// let pool = PgPool::connect(url).await?; -// Ok(pool) -// } - -// fn to_pg_pool(&self) -> PgPool { -// self.clone() -// } -// } - -// #[async_trait] -// // #[cfg_attr(test, automock)] -// pub trait GuildRepository { -// fn new_guild(id: i64, name: String) -> Guild; -// async fn get(&self, pool: &dyn ConnectionPool, guild_id: i64) -> Result, Error>; -// async fn get_or_create( -// &self, -// pool: &dyn ConnectionPool, -// guild_id: i64, -// name: String, -// ) -> Result; -// } - -// #[async_trait] -// impl GuildRepository for Guild { -// fn new_guild(id: i64, name: String) -> Guild { -// Guild { -// id, -// name, -// created_at: chrono::Utc::now().naive_utc(), -// updated_at: chrono::Utc::now().naive_utc(), -// } -// } - -// async fn get(&self, pool: &impl ConnectionPool, guild_id: i64) -> Result, Error> { -// let pool = pool.to_pg_pool(); -// let guild = sqlx::query_as!( -// Guild, -// r#" -// SELECT * FROM guild -// WHERE id = $1 -// "#, -// guild_id -// ) -// .fetch_optional(&pool) -// .await?; - -// Ok(guild) -// } - -// async fn get_or_create( -// &self, -// pool: &impl ConnectionPool, -// guild_id: i64, -// name: String, -// ) -> Result { -// let pool = pool.to_pg_pool(); -// let guild = sqlx::query_as!( -// Guild, -// r#" -// INSERT INTO guild (id, name) -// VALUES ($1, $2) -// ON CONFLICT (id) -// DO UPDATE SET name = $2, updated_at = now() -// RETURNING * -// "#, -// guild_id, -// name -// ) -// .fetch_one(&pool) -// .await?; - -// Ok(guild) -// } -// } -// #[cfg(test)] -// mod test { -// // Mock the GuildRepository trait -// use super::*; -// use mockall::predicate::*; -// use mockall::*; - -// mock! { -// ConnectionPool{} - -// #[async_trait] -// impl ConnectionPool for ConnectionPool { -// async fn connect(&self, url: &str) -> Result; -// fn to_pg_pool(&self) -> PgPool; -// } -// } -// mock! { -// Guild{} - -// #[async_trait] -// impl GuildRepository for Guild { -// fn new_guild(id: i64, name: String) -> Guild; -// async fn get(&self, pool: &dyn ConnectionPool, guild_id: i64) -> Result, Error>; -// async fn get_or_create(&self, pool: &dyn ConnectionPool, guild_id: i64, name: String) -> Result; -// } -// } - -// #[tokio::test] -// async fn test_get_or_create() { -// // let mock = MockGuild::new(1, "asdf".to_string()); -// let mock_guild = MockGuild::new(); -// // let pool = mock(PgPool::connect("postgres://localhost").await.unwrap()); -// let pool = MockConnectionPool::new(); -// let pool = pool.connect("postgres://localhost").await.unwrap(); -// mock_guild -// .expect_get_or_create() -// .with(&pool, eq(1), eq("test2".to_string())) -// .returning(|_, _, _| Ok(Guild::new_guild(1, "test2".to_string()))); - -// let guild = mock_guild -// .get_or_create(&pool, 1, "test2".to_string()) -// .await -// .unwrap(); -// assert_eq!(guild.name, "test2"); - -// let guild = mock_guild.get(&pool, 1).await.unwrap(); -// assert!(guild.is_some()); -// } -// } + pub async fn load_command_settings( + guild_id: i64, + pool: &PgPool, + ) -> CrackedResult> { + sqlx::query_as!( + GenericPermissionSettingsReadWCommand, + r#" + SELECT A.command, permission_settings.* FROM + (SELECT * FROM command_channel WHERE guild_id = $1) as A + JOIN permission_settings ON A.permission_settings_id = permission_settings.id + "#, + guild_id, + ) + .fetch_all(pool) + .await + .map(|rows| { + rows.into_iter() + .map(|row| (row.command.clone(), GenericPermissionSettings::from(row))) + .collect() + }) + .map_err(|e| e.into()) + } +} diff --git a/crack-core/src/db/metadata.rs b/crack-core/src/db/metadata.rs index 107cf9ae6..b8c9aaf88 100644 --- a/crack-core/src/db/metadata.rs +++ b/crack-core/src/db/metadata.rs @@ -220,12 +220,17 @@ pub async fn playlist_track_to_metadata( use crate::db; use super::PlaylistTrack; + +pub enum MetadataAnd { + Track(Metadata, PlaylistTrack), +} + /// Convert an `AuxMetadata` structure to the database structures. pub fn aux_metadata_to_db_structures( metadata: &AuxMetadata, guild_id: i64, channel_id: i64, -) -> Result<(Metadata, db::PlaylistTrack), CrackedError> { +) -> Result { let track = metadata.track.clone(); let title = metadata.title.clone(); let artist = metadata.artist.clone(); @@ -272,7 +277,7 @@ pub fn aux_metadata_to_db_structures( channel_id: Some(channel_id), }; - Ok((metadata, db_track)) + Ok(MetadataAnd::Track(metadata, db_track)) } /// Convert an `AuxMetadata` structure to the database structures. @@ -323,3 +328,31 @@ pub fn aux_metadata_from_db(metadata: &Metadata) -> Result for AuxMetadata { + fn from(metadata: Metadata) -> Self { + aux_metadata_from_db(&metadata).unwrap() + } +} + +impl From for Metadata { + fn from(metadata: AuxMetadata) -> Self { + aux_metadata_to_db_structures(&metadata, 0, 0) + .map(|MetadataAnd::Track(metadata, _)| metadata) + .unwrap() + } +} + +impl From for db::PlaylistTrack { + fn from(metadata: AuxMetadata) -> Self { + aux_metadata_to_db_structures(&metadata, 0, 0) + .map(|MetadataAnd::Track(_, playlist_track)| playlist_track) + .unwrap() + } +} + +impl From for MetadataAnd { + fn from(metadata: AuxMetadata) -> Self { + aux_metadata_to_db_structures(&metadata, 0, 0).unwrap() + } +} diff --git a/crack-core/src/db/play_log.rs b/crack-core/src/db/play_log.rs index 95047dd4c..2ed0998d0 100644 --- a/crack-core/src/db/play_log.rs +++ b/crack-core/src/db/play_log.rs @@ -1,6 +1,7 @@ use std::fmt::{Display, Formatter}; use poise::futures_util::StreamExt; +use poise::serenity_prelude as serenity; use sqlx::types::chrono::NaiveDateTime; use sqlx::{Error, PgPool}; @@ -15,6 +16,35 @@ pub struct PlayLog { pub created_at: NaiveDateTime, } +#[derive(Debug, Clone)] +pub struct PlayLogQuery { + pub user_id: Option, + pub guild_id: Option, + pub limit: Option, + pub max_dislikes: Option, +} + +pub trait PgPoolExtPlayLog { + fn insert_playlog_entry( + &self, + user_id: serenity::UserId, + guild_id: serenity::GuildId, + metadata_id: i64, + //) -> std::pin::Pin> + Send>>; + ) -> impl std::future::Future>; + + fn get_last_played( + &self, + query: &PlayLogQuery, + ) -> impl std::future::Future, Error>>; + + fn get_last_played_by_guild( + &self, + guild_id: serenity::GuildId, + limit: i64, + ) -> impl std::future::Future, Error>>; +} + #[derive(Debug, Clone)] struct TitleArtist { title: Option, @@ -32,6 +62,56 @@ impl Display for TitleArtist { } } +impl PgPoolExtPlayLog for PgPool { + async fn insert_playlog_entry( + &self, + user_id: serenity::UserId, + guild_id: serenity::GuildId, + metadata_id: i64, + ) -> Result { + PlayLog::create( + self, + user_id.get() as i64, + guild_id.get() as i64, + metadata_id, + ) + .await + } + + async fn get_last_played(&self, query: &PlayLogQuery) -> Result, Error> { + match (&query.user_id, &query.guild_id) { + (Some(user_id), None) => { + PlayLog::get_last_played_by_user(self, *user_id, query.limit.unwrap_or(i64::MAX)) + .await + }, + (None, Some(guild_id)) => { + PlayLog::get_last_played_by_guild_filter_limit( + self, + *guild_id, + query.max_dislikes.unwrap_or(1), + query.limit.unwrap_or(i64::MAX), + ) + .await + }, + _ => Ok(vec![]), + } + } + + async fn get_last_played_by_guild( + &self, + guild_id: serenity::GuildId, + limit: i64, + ) -> Result, Error> { + let query = PlayLogQuery { + user_id: None, + guild_id: Some(guild_id.get() as i64), + limit: Some(limit), + max_dislikes: None, + }; + self.get_last_played(&query).await + } +} + impl PlayLog { /// Create a new play log entry. pub async fn create( @@ -56,6 +136,15 @@ impl PlayLog { Ok(play_log) } + /// Get the last played track for the given user and guild. + pub async fn get_last_played_by_guild_limit( + conn: &PgPool, + guild_id: i64, + limit: i64, + ) -> Result, Error> { + Self::get_last_played_by_guild_filter_limit(conn, guild_id, 0, limit).await + } + /// Get the last played track for the given user and guild. pub async fn get_last_played( conn: &PgPool, @@ -67,7 +156,7 @@ impl PlayLog { } else if user_id.is_none() && guild_id.is_some() { Self::get_last_played_by_guild(conn, guild_id.unwrap()).await } else { - Self::get_last_played_by_user(conn, user_id.unwrap()).await + Self::get_last_played_by_user(conn, user_id.unwrap(), 100).await } } @@ -77,6 +166,16 @@ impl PlayLog { guild_id: i64, max_dislikes: i32, ) -> Result, Error> { + PlayLog::get_last_played_by_guild_filter_limit(conn, guild_id, max_dislikes, 100).await + } + + pub async fn get_last_played_by_guild_filter_limit( + conn: &PgPool, + guild_id: i64, + max_dislikes: i32, + limit: i64, + ) -> Result, Error> { + let max_dislikes = if max_dislikes < 0 { 1 } else { max_dislikes }; //let last_played: Vec = sqlx::query_as!( let mut last_played: Vec = Vec::new(); let mut last_played_stream = sqlx::query_as!( @@ -87,11 +186,12 @@ impl PlayLog { join metadata on play_log.metadata_id = metadata.id) left join track_reaction on play_log.id = track_reaction.play_log_id - where guild_id = $1 and (track_reaction is null or track_reaction.dislikes >= $2) - order by play_log.created_at desc limit 5 + where guild_id = $1 and (track_reaction is null or track_reaction.dislikes <= $2) + order by play_log.created_at desc limit $3 "#, guild_id, - max_dislikes + max_dislikes, + limit ) .fetch(conn); while let Some(item) = last_played_stream.next().await { @@ -113,7 +213,11 @@ impl PlayLog { pub async fn get_last_played_by_guild_metadata( conn: &PgPool, guild_id: i64, + limit: i64, ) -> Result, Error> { + if limit <= 0 { + return Ok(vec![]); + } let mut last_played: Vec = Vec::new(); let mut last_played_stream = sqlx::query_as!( Metadata, @@ -122,9 +226,10 @@ impl PlayLog { from play_log join metadata on play_log.metadata_id = metadata.id - where guild_id = $1 order by created_at desc limit 5 + where guild_id = $1 order by created_at desc limit $2 "#, - guild_id + guild_id, + limit ) .fetch(conn); while let Some(item) = last_played_stream.next().await { @@ -138,7 +243,11 @@ impl PlayLog { pub async fn get_last_played_by_user( conn: &PgPool, user_id: i64, + limit: i64, ) -> Result, Error> { + if limit < 0 { + return Ok(vec![]); + } //let last_played: Vec = sqlx::query_as!( let mut last_played: Vec = Vec::new(); let mut last_played_stream = sqlx::query_as!( @@ -148,9 +257,10 @@ impl PlayLog { from play_log join metadata on play_log.metadata_id = metadata.id - where user_id = $1 order by created_at desc limit 5 + where user_id = $1 order by created_at desc limit $2 "#, - user_id + user_id, + limit ) .fetch(conn); while let Some(item) = last_played_stream.next().await { diff --git a/crack-core/src/db/user.rs b/crack-core/src/db/user.rs index e0dab0916..6136880fb 100644 --- a/crack-core/src/db/user.rs +++ b/crack-core/src/db/user.rs @@ -1,4 +1,5 @@ use crate::errors::CrackedError; +use crate::messaging::messages::TEST; use ::chrono::Duration; use sqlx::{ types::chrono::{self}, @@ -38,7 +39,7 @@ pub struct UserVote { impl User { /// Insert a test user into the database. pub async fn insert_test_user(pool: &PgPool, user_id: Option, username: Option) { - let user = username.unwrap_or("test".to_string()); + let user = username.unwrap_or(TEST.to_string()); let user_id = user_id.unwrap_or(1); let result = sqlx::query!( r#"insert into public.user @@ -170,6 +171,7 @@ impl UserVote { #[cfg(test)] mod test { use super::UserVote; + use super::TEST; use crate::db::User; use chrono::{Duration, Utc}; use sqlx::PgPool; @@ -189,23 +191,23 @@ mod test { #[sqlx::test(migrator = "MIGRATOR")] async fn test_insert_user(pool: PgPool) { - User::insert_test_user(&pool, Some(1), Some("test".to_string())).await; + User::insert_test_user(&pool, Some(1), Some(TEST.to_string())).await; let user = User::get_user(&pool, 1).await.unwrap(); - assert_eq!(user.username, "test"); + assert_eq!(user.username, TEST); } #[sqlx::test(migrator = "MIGRATOR")] async fn test_insert_or_update_user(pool: PgPool) { - User::insert_or_update_user(&pool, 1, "test".to_string()) + User::insert_or_update_user(&pool, 1, TEST.to_string()) .await .unwrap(); let user = User::get_user(&pool, 1).await.unwrap(); - assert_eq!(user.username, "test"); + assert_eq!(user.username, TEST); } #[sqlx::test(migrator = "MIGRATOR")] async fn test_insert_user_vote(pool: PgPool) { - let insert_res = UserVote::insert_user_vote(&pool, 1, "test".to_string()).await; + let insert_res = UserVote::insert_user_vote(&pool, 1, TEST.to_string()).await; assert!(insert_res.is_ok()); let user_votes = UserVote::get_user_votes(1, &pool).await; assert!(user_votes.is_ok()); @@ -213,17 +215,17 @@ mod test { assert_eq!(user_votes.len(), 1); let first = user_votes.first(); assert!(first.is_some()); - assert_eq!(first.unwrap().site, "test"); + assert_eq!(first.unwrap().site, TEST); } #[sqlx::test(migrator = "MIGRATOR")] async fn test_has_voted_recently(pool: PgPool) { - UserVote::insert_user_vote(&pool, 1, "test".to_string()) + UserVote::insert_user_vote(&pool, 1, TEST.to_string()) .await .unwrap(); let has_voted = UserVote::has_voted_recently( 1, - "test".to_string(), + TEST.to_string(), Utc::now() .naive_utc() .checked_add_signed(Duration::seconds(-5 * 60)) diff --git a/crack-core/src/db/worker_pool.rs b/crack-core/src/db/worker_pool.rs index 2a9759d07..ea29f9ca5 100644 --- a/crack-core/src/db/worker_pool.rs +++ b/crack-core/src/db/worker_pool.rs @@ -31,7 +31,7 @@ impl Display for MetadataMsg { ) } } - +use crate::db::metadata::MetadataAnd; /// Writes metadata to the database for a playing track. pub async fn write_metadata_pg( database_pool: &PgPool, @@ -45,7 +45,7 @@ pub async fn write_metadata_pg( channel_id, } = data; let returned_metadata = { - let (metadata, _playlist_track) = match aux_metadata_to_db_structures( + let MetadataAnd::Track(metadata, _) = match aux_metadata_to_db_structures( &aux_metadata, guild_id.get() as i64, channel_id.get() as i64, diff --git a/crack-core/src/errors.rs b/crack-core/src/errors.rs index 0c96c333a..c1e7b5715 100644 --- a/crack-core/src/errors.rs +++ b/crack-core/src/errors.rs @@ -2,8 +2,8 @@ use crate::messaging::messages::{ EMPTY_SEARCH_RESULT, FAIL_ANOTHER_CHANNEL, FAIL_AUDIO_STREAM_RUSTY_YTDL_METADATA, FAIL_AUTHOR_DISCONNECTED, FAIL_AUTHOR_NOT_FOUND, FAIL_EMPTY_VECTOR, FAIL_INSERT, FAIL_INVALID_PERMS, FAIL_INVALID_TOPGG_TOKEN, FAIL_NOTHING_PLAYING, FAIL_NOT_IMPLEMENTED, - FAIL_NO_SONGBIRD, FAIL_NO_VIRUSTOTAL_API_KEY, FAIL_NO_VOICE_CONNECTION, FAIL_PARSE_TIME, - FAIL_PLAYLIST_FETCH, FAIL_TO_SET_CHANNEL_SIZE, FAIL_WRONG_CHANNEL, GUILD_ONLY, + FAIL_NO_QUERY_PROVIDED, FAIL_NO_SONGBIRD, FAIL_NO_VIRUSTOTAL_API_KEY, FAIL_NO_VOICE_CONNECTION, + FAIL_PARSE_TIME, FAIL_PLAYLIST_FETCH, FAIL_TO_SET_CHANNEL_SIZE, FAIL_WRONG_CHANNEL, GUILD_ONLY, NOT_IN_MUSIC_CHANNEL, NO_CHANNEL_ID, NO_DATABASE_POOL, NO_GUILD_CACHED, NO_GUILD_ID, NO_GUILD_SETTINGS, NO_USER_AUTOPLAY, QUEUE_IS_EMPTY, ROLE_NOT_FOUND, SPOTIFY_AUTH_FAILED, UNAUTHORIZED_USER, @@ -21,6 +21,7 @@ use songbird::input::AudioStreamError; use std::fmt::{self}; use std::fmt::{Debug, Display}; use std::process::ExitStatus; +use tokio::time::error::Elapsed; /// A common error enum returned by most of the crate's functions within a [`Result`]. #[derive(Debug)] @@ -34,6 +35,7 @@ pub enum CrackedError { #[cfg(feature = "crack-gpt")] CrackGPT(Error), CommandFailed(String, ExitStatus, String), + CommandNotFound(String), DurationParseError(String, String), EmptySearchResult, EmptyVector(&'static str), @@ -63,6 +65,7 @@ pub enum CrackedError { NothingPlaying, NoSongbird, NoVirusTotalApiKey, + NoQuery, Other(&'static str), PlayListFail, ParseTimeFail, @@ -120,6 +123,9 @@ impl Display for CrackedError { Self::CommandFailed(program, status, output) => f.write_str(&format!( "Command `{program}` failed with status `{status}` and output `{output}`" )), + Self::CommandNotFound(command) => { + f.write_fmt(format_args!("Command does not exist: {}", command)) + }, Self::DurationParseError(d, u) => { f.write_str(&format!("Failed to parse duration `{d}` and `{u}`",)) }, @@ -163,6 +169,7 @@ impl Display for CrackedError { Self::NothingPlaying => f.write_str(FAIL_NOTHING_PLAYING), Self::NoSongbird => f.write_str(FAIL_NO_SONGBIRD), Self::NoVirusTotalApiKey => f.write_str(FAIL_NO_VIRUSTOTAL_API_KEY), + Self::NoQuery => f.write_str(FAIL_NO_QUERY_PROVIDED), Self::Other(msg) => f.write_str(msg), Self::PlayListFail => f.write_str(FAIL_PLAYLIST_FETCH), @@ -216,6 +223,13 @@ impl PartialEq for CrackedError { } } +/// Provides an implementation to convert a [`anyhow::Error`] to a [`CrackedError`]. +impl From for CrackedError { + fn from(err: anyhow::Error) -> Self { + Self::Anyhow(err) + } +} + /// Provides an implementation to convert a [`VideoError`] to a [`CrackedError`]. impl From for CrackedError { fn from(err: VideoError) -> Self { @@ -313,6 +327,20 @@ impl From for CrackedError { } } +// /// Provides an implementation to convert a [`tokio::time::error::Elapsed`] to a [`CrackedError`]. +// impl From for CrackedError { +// fn from(err: JoinError) -> Self { +// CrackedError::JoinChannelError(err) +// } +// } + +/// Provides an implementation to convert a [`Elapsed`] to a [`CrackedError`]. +impl From for CrackedError { + fn from(_err: Elapsed) -> Self { + CrackedError::Other("Timeout") + } +} + /// Types that implement this trait can be tested as true or false and also provide /// a way of unpacking themselves. pub trait Verifiable { @@ -365,6 +393,8 @@ pub fn verify>(verifiable: T, err: CrackedError) -> Result impl Future>; fn set_guild_settings( @@ -40,8 +42,13 @@ pub trait GuildSettingsOperations { ) -> impl Future; fn get_autopause(&self, guild_id: GuildId) -> impl Future; fn set_autopause(&self, guild_id: GuildId, autopause: bool) -> impl Future; + fn toggle_autopause(&self, guild_id: GuildId) -> impl Future; + fn get_auto_role(&self, guild_id: GuildId) -> impl Future>; + fn set_auto_role(&self, guild_id: GuildId, auto_role: u64) -> impl Future; fn get_autoplay(&self, guild_id: GuildId) -> impl Future; fn set_autoplay(&self, guild_id: GuildId, autoplay: bool) -> impl Future; + fn get_volume(&self, guild_id: GuildId) -> impl Future; + fn set_volume(&self, guild_id: GuildId, volume: u64) -> impl Future; fn get_reply_with_embed(&self, guild_id: GuildId) -> impl Future; fn set_reply_with_embed(&self, guild_id: GuildId, as_embed: bool) -> impl Future; @@ -86,23 +93,15 @@ impl GuildSettingsOperations for Data { .read() .await .get(&guild_id) - .and_then(|x| { - x.command_channels - .music_channel - .as_ref() - .map(|x| x.channel_id) - }) + .and_then(|x| x.get_music_channel()) } /// Set the music channel for the guild. async fn set_music_channel(&self, guild_id: GuildId, channel_id: ChannelId) { - self.guild_settings_map - .write() - .await - .entry(guild_id) - .and_modify(|e| { - e.set_music_channel(channel_id.get()); - }); + let mut guard = self.guild_settings_map.write().await; + let _ = guard + .get_mut(&guild_id) + .map(|x| x.set_music_channel(channel_id.get())); } /// Save the guild settings to the database. @@ -228,6 +227,40 @@ impl GuildSettingsOperations for Data { }); } + /// Toggle the autopause setting. + async fn toggle_autopause(&self, guild_id: GuildId) -> bool { + self.guild_settings_map + .write() + .await + .entry(guild_id) + .and_modify(|e| { + e.autopause = !e.autopause; + }) + .or_insert_with(Default::default) + .autopause + } + + /// Get the current auto role for the guild. + async fn get_auto_role(&self, guild_id: GuildId) -> Option { + self.guild_settings_map + .read() + .await + .get(&guild_id) + .and_then(|x| x.welcome_settings.as_ref()) + .and_then(|x| x.auto_role) + } + + /// Set the auto role for the guild. + async fn set_auto_role(&self, guild_id: GuildId, auto_role: u64) { + self.guild_settings_map + .write() + .await + .entry(guild_id) + .and_modify(|e| { + e.set_auto_role(Some(auto_role)); + }); + } + /// Get the current autoplay settings. async fn get_autoplay(&self, guild_id: GuildId) -> bool { self.guild_cache_map @@ -248,6 +281,33 @@ impl GuildSettingsOperations for Data { .autoplay = autoplay; } + /// Get the current autoplay settings. + async fn get_volume(&self, guild_id: GuildId) -> (f32, f32) { + self.guild_settings_map + .read() + .await + .get(&guild_id) + .map(|settings| (settings.volume, settings.old_volume)) + .unwrap_or((DEFAULT_VOLUME_LEVEL, DEFAULT_VOLUME_LEVEL)) + } + + /// Set the current autoplay settings. + async fn set_volume(&self, guild_id: GuildId, vol: u64) { + self.guild_settings_map + .write() + .await + .entry(guild_id) + .and_modify(|e| { + e.old_volume = e.volume; + e.volume = vol as f32; + }) + .or_insert_with(|| GuildSettings { + volume: vol as f32, + old_volume: vol as f32, + ..Default::default() + }); + } + /// Get the current reply with embed setting. async fn get_reply_with_embed(&self, guild_id: GuildId) -> bool { self.guild_settings_map @@ -282,10 +342,7 @@ pub async fn get_guilds(ctx: Arc) -> Vec { #[cfg(test)] mod test { use super::*; - use crate::{ - guild::{permissions::CommandChannel, settings::CommandChannels}, - Data, DataInner, - }; + use crate::{Data, DataInner}; use serenity::model::id::ChannelId; use std::collections::HashMap; use tokio::sync::RwLock; @@ -368,21 +425,9 @@ mod test { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); let channel_id = ChannelId::new(2); - guild_settings_map.insert( - guild_id, - crate::GuildSettings { - command_channels: CommandChannels { - music_channel: Some(CommandChannel { - command: "".to_string(), - guild_id, - channel_id, - permission_settings: Default::default(), - }), - ..Default::default() - }, - ..Default::default() - }, - ); + let mut settings = crate::GuildSettings::default(); + settings.set_music_channel(channel_id.get()); + guild_settings_map.insert(guild_id, settings); let data = Arc::new(Data(Arc::new(DataInner { guild_settings_map: Arc::new(RwLock::new(guild_settings_map)), ..Default::default() @@ -396,21 +441,9 @@ mod test { let mut guild_settings_map = HashMap::new(); let guild_id = GuildId::new(1); let channel_id = ChannelId::new(2); - guild_settings_map.insert( - guild_id, - crate::GuildSettings { - command_channels: CommandChannels { - music_channel: Some(CommandChannel { - command: "".to_string(), - guild_id, - channel_id, - permission_settings: Default::default(), - }), - ..Default::default() - }, - ..Default::default() - }, - ); + let mut settings = crate::GuildSettings::default(); + settings.set_music_channel(channel_id.get()); + guild_settings_map.insert(guild_id, settings); let data = Arc::new(Data(Arc::new(DataInner { guild_settings_map: Arc::new(RwLock::new(guild_settings_map)), ..Default::default() diff --git a/crack-core/src/guild/permissions.rs b/crack-core/src/guild/permissions.rs index 132f23bab..6031e9d26 100644 --- a/crack-core/src/guild/permissions.rs +++ b/crack-core/src/guild/permissions.rs @@ -3,7 +3,7 @@ use serenity::all::{ChannelId, GuildId}; use sqlx::{FromRow, PgPool}; use std::collections::HashSet; -use crate::{errors::CrackedError, Error}; +use crate::{errors::CrackedError, Context, Error}; /// Type alias for a HashSet of strings. type HashSetString = HashSet; @@ -66,16 +66,57 @@ pub struct GenericPermissionSettings { pub default_allow_all_users: bool, #[serde(default = "default_true")] pub default_allow_all_roles: bool, - // pub allowed_commands: HashSet, - // pub denied_commands: HashSet, pub allowed_roles: HashSet, pub denied_roles: HashSet, pub allowed_users: HashSet, pub denied_users: HashSet, + #[serde(default)] + pub allowed_channels: HashSet, + #[serde(default)] + pub denied_channels: HashSet, + // pub allowed_commands: HashSet, + // pub denied_commands: HashSet, +} + +impl From for GenericPermissionSettings { + fn from(read: GenericPermissionSettingsReadWCommand) -> Self { + Self { + id: read.id, + default_allow_all_commands: read.default_allow_all_commands, + default_allow_all_users: read.default_allow_all_users, + default_allow_all_roles: read.default_allow_all_roles, + allowed_roles: read.allowed_roles.convert(), + denied_roles: read.denied_roles.convert(), + allowed_users: read.allowed_users.convert(), + denied_users: read.denied_users.convert(), + allowed_channels: read.allowed_channels.convert(), + denied_channels: read.denied_channels.convert(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct GenericPermissionSettingsReadWCommand { + pub command: String, + pub id: i64, + #[serde(default = "default_true")] + pub default_allow_all_commands: bool, + #[serde(default = "default_true")] + pub default_allow_all_users: bool, + #[serde(default = "default_true")] + pub default_allow_all_roles: bool, + // pub allowed_commands: serde_json::Value, + // pub denied_commands: serde_json::Value, + pub allowed_roles: Vec, + pub denied_roles: Vec, + pub allowed_users: Vec, + pub denied_users: Vec, + pub allowed_channels: Vec, + pub denied_channels: Vec, } /// Struct for reading generic permission settings from a pg table. -#[derive(Serialize, Deserialize, FromRow)] +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct GenericPermissionSettingsRead { pub id: i64, #[serde(default = "default_true")] @@ -90,6 +131,8 @@ pub struct GenericPermissionSettingsRead { pub denied_roles: Vec, pub allowed_users: Vec, pub denied_users: Vec, + pub allowed_channels: Vec, + pub denied_channels: Vec, } /// Implementation of GenericPermissionSettingsRead. @@ -106,6 +149,8 @@ impl GenericPermissionSettingsRead { denied_roles: self.denied_roles.convert(), allowed_users: self.allowed_users.convert(), denied_users: self.denied_users.convert(), + allowed_channels: self.allowed_channels.convert(), + denied_channels: self.denied_channels.convert(), } } } @@ -129,6 +174,8 @@ impl Default for GenericPermissionSettings { denied_roles: HashSet::new(), allowed_users: HashSet::new(), denied_users: HashSet::new(), + allowed_channels: HashSet::new(), + denied_channels: HashSet::new(), } } } @@ -172,6 +219,13 @@ impl GenericPermissionSettings { || self.allowed_users.contains(&user) && !self.denied_users.contains(&user) } + pub fn is_channel_allowed(&self, channel: u64) -> bool { + (self.allowed_channels.is_empty() && self.denied_channels.is_empty()) + || (self.allowed_channels.is_empty() && !self.denied_channels.contains(&channel)) + || (self.allowed_channels.contains(&channel) + && !self.denied_channels.contains(&channel)) + } + // /// Add a command to the allowed commands. // pub fn add_allowed_command(&mut self, command: String) { // self.allowed_commands.insert(command); @@ -193,43 +247,63 @@ impl GenericPermissionSettings { // } /// Add a role to the allowed roles. - pub fn add_allowed_role(&mut self, role: u64) { - self.allowed_roles.insert(role); + pub fn add_allowed_role(&mut self, role: u64) -> bool { + self.allowed_roles.insert(role) } /// Remove a role from the allowed roles. - pub fn remove_allowed_role(&mut self, role: u64) { - self.allowed_roles.remove(&role); + pub fn remove_allowed_role(&mut self, role: u64) -> bool { + self.allowed_roles.remove(&role) } /// Add a role to the denied roles. - pub fn add_denied_role(&mut self, role: u64) { - self.denied_roles.insert(role); + pub fn add_denied_role(&mut self, role: u64) -> bool { + self.denied_roles.insert(role) } /// Remove a role from the denied roles. - pub fn remove_denied_role(&mut self, role: u64) { - self.denied_roles.remove(&role); + pub fn remove_denied_role(&mut self, role: u64) -> bool { + self.denied_roles.remove(&role) } /// Add a user to the allowed users. - pub fn add_allowed_user(&mut self, user: u64) { - self.allowed_users.insert(user); + pub fn add_allowed_user(&mut self, user: u64) -> bool { + self.allowed_users.insert(user) } /// Remove a user from the allowed users. - pub fn remove_allowed_user(&mut self, user: u64) { - self.allowed_users.remove(&user); + pub fn remove_allowed_user(&mut self, user: u64) -> bool { + self.allowed_users.remove(&user) } /// Add a user to the denied users. - pub fn add_denied_user(&mut self, user: u64) { - self.denied_users.insert(user); + pub fn add_denied_user(&mut self, user: u64) -> bool { + self.denied_users.insert(user) } /// Remove a user from the denied users. - pub fn remove_denied_user(&mut self, user: u64) { - self.denied_users.remove(&user); + pub fn remove_denied_user(&mut self, user: u64) -> bool { + self.denied_users.remove(&user) + } + + /// Add a channel to the allowed channels. + pub fn add_allowed_channel(&mut self, channel: u64) -> bool { + self.allowed_channels.insert(channel) + } + + /// Remove a channel from the allowed channels. + pub fn remove_allowed_channel(&mut self, channel: u64) -> bool { + self.allowed_channels.remove(&channel) + } + + /// Add a channel to the denied channels. + pub fn add_denied_channel(&mut self, channel: u64) -> bool { + self.denied_channels.insert(channel) + } + + /// Remove a channel from the denied channels. + pub fn remove_denied_channel(&mut self, channel: u64) -> bool { + self.denied_channels.remove(&channel) } /// Clear all allowed and denied commands, roles, and users. @@ -240,12 +314,14 @@ impl GenericPermissionSettings { self.denied_roles.clear(); self.allowed_users.clear(); self.denied_users.clear(); + self.allowed_channels.clear(); + self.denied_channels.clear(); } /// Write to a pg table. pub async fn insert_permission_settings( + &self, pool: &PgPool, - settings: &GenericPermissionSettings, ) -> Result { sqlx::query_as!( GenericPermissionSettingsRead, @@ -256,35 +332,47 @@ impl GenericPermissionSettings { allowed_roles, denied_roles, allowed_users, - denied_users) + denied_users, + allowed_channels, + denied_channels) VALUES - ($1, $2, $3, $4, $5, $6, $7) + ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING *", - settings.default_allow_all_commands, - settings.default_allow_all_users, - settings.default_allow_all_roles, + self.default_allow_all_commands, + self.default_allow_all_users, + self.default_allow_all_roles, // json!(settings.allowed_commands) as serde_json::Value, // Convert to JSON // json!(settings.denied_commands) as serde_json::Value, - &settings + &self .allowed_roles .iter() .map(|&x| x as i64) .collect::>(), // Convert to Vec - &settings + &self .denied_roles .iter() .map(|&x| x as i64) .collect::>(), - &settings + &self .allowed_users .iter() .map(|&x| x as i64) .collect::>(), - &settings + &self .denied_users .iter() .map(|&x| x as i64) .collect::>(), + &self + .allowed_channels + .iter() + .map(|&x| x as i64) + .collect::>(), + &self + .denied_channels + .iter() + .map(|&x| x as i64) + .collect::>(), ) .fetch_one(pool) .await @@ -327,6 +415,17 @@ pub struct CommandChannelRead { pub permission_settings_id: i64, } +impl Default for CommandChannel { + fn default() -> Self { + Self { + command: "".to_string(), + channel_id: ChannelId::new(0), + guild_id: GuildId::new(0), + permission_settings: GenericPermissionSettings::default(), + } + } +} + impl CommandChannel { /// Convert a CommandChannelRead to a CommandChannel. pub async fn from_command_channel_read( @@ -348,11 +447,13 @@ impl CommandChannel { /// Insert a CommandChannel into a pg table. pub async fn insert_command_channel(&self, pool: &PgPool) -> Result { - let mut settings = self.permission_settings.clone(); - if settings.id == 0 { - settings = - GenericPermissionSettings::insert_permission_settings(pool, &settings).await?; - } + let settings = if self.permission_settings.id == 0 { + self.permission_settings + .insert_permission_settings(pool) + .await? + } else { + self.permission_settings.clone() + }; let command_channel = sqlx::query_as!( CommandChannelRead, r#"INSERT INTO command_channel @@ -371,10 +472,9 @@ impl CommandChannel { ) .fetch_one(pool) .await?; - let command_channel = CommandChannel::from_command_channel_read(pool, command_channel) + CommandChannel::from_command_channel_read(pool, command_channel) .await - .unwrap(); - Ok(command_channel) + .map_err(Into::into) } pub async fn save(&self, pool: &PgPool) -> Result { @@ -410,6 +510,28 @@ impl CommandChannel { } } +pub async fn command_check_music(ctx: Context<'_>) -> Result { + if ctx.author().bot { + return Ok(false); + }; + + // let data: &Data = ctx.data(); + // let user_row = data.userinfo_db.get(ctx.author().id.into()).await?; + // if user_row.bot_banned() { + // notify_banned(ctx).await?; + // return Ok(false); + // } + + let Some(guild_id) = ctx.guild_id() else { + return Ok(true); + }; + + Ok(ctx + .data() + .check_music_permissions(guild_id, ctx.author().id) + .await) +} + #[cfg(test)] mod tests { use super::*; @@ -422,7 +544,8 @@ mod tests { fn set_env() { use std::env; if env::var("DATABASE_URL").is_err() { - env::set_var("DATABASE_URL", "postgresql://localhost:5432/postgres"); + // env::set_var("DATABASE_URL", "postgresql://localhost:5432/postgres"); + println!("WARNING: DATABASE_URL not set for tests"); } } @@ -545,13 +668,17 @@ mod tests { denied_roles: vec![1], allowed_users: vec![1, 2], denied_users: vec![1], + allowed_channels: vec![1, 2], + denied_channels: vec![1], }; let settings = settings_read.convert(); // assert!(settings.is_command_allowed("test")); assert!(!settings.is_role_allowed(1)); assert!(!settings.is_user_allowed(1)); + assert!(!settings.is_channel_allowed(1)); assert!(settings.is_role_allowed(2)); assert!(settings.is_user_allowed(2)); + assert!(settings.is_channel_allowed(2)); } #[test] @@ -582,9 +709,7 @@ mod tests { // settings.add_denied_command("test2".to_string()); settings.add_allowed_role(1); settings.add_allowed_user(1); - GenericPermissionSettings::insert_permission_settings(&pool, &settings) - .await - .unwrap(); + settings.insert_permission_settings(&pool).await.unwrap(); let settings_read = GenericPermissionSettings::get_permission_settings(&pool, 1) .await diff --git a/crack-core/src/guild/settings.rs b/crack-core/src/guild/settings.rs index 39f76d643..ae91f0a8f 100644 --- a/crack-core/src/guild/settings.rs +++ b/crack-core/src/guild/settings.rs @@ -2,26 +2,29 @@ use self::serenity::model::id::GuildId; use self::serenity::model::prelude::UserId; use crate::db::{GuildEntity, WelcomeSettingsRead}; use crate::errors::CrackedError; +use crate::CrackedResult; use lazy_static::lazy_static; use poise::serenity_prelude::{self as serenity, ChannelId, FullEvent}; use serde::{Deserialize, Serialize}; use sqlx::PgPool; use std::collections::BTreeMap; use std::fmt::{Display, Formatter}; +use std::fs::{create_dir_all, OpenOptions}; use std::io::Write; use std::sync::{atomic, Arc}; use std::{ collections::{HashMap, HashSet}, env, - fs::{create_dir_all, OpenOptions}, }; use typemap_rev::TypeMapKey; -use super::permissions::{CommandChannel, GenericPermissionSettings}; - -pub const DEFAULT_LOG_PREFIX: &str = "data/logs"; +pub(crate) const DEFAULT_LOG_PREFIX: &str = "data/logs"; pub(crate) const DEFAULT_ALLOW_ALL_DOMAINS: bool = true; pub(crate) const DEFAULT_SETTINGS_PATH: &str = "data/settings"; +#[allow(dead_code)] +pub(crate) const PIPED_WATCH_URL: &str = "https://piped.video/watch?v="; +pub(crate) const YOUTUBE_WATCH_URL: &str = "https://www.youtube.com/watch?v="; +pub(crate) const VIDEO_WATCH_URL: &str = YOUTUBE_WATCH_URL; pub(crate) const DEFAULT_ALLOWED_DOMAINS: [&str; 1] = ["youtube.com"]; pub(crate) const DEFAULT_VOLUME_LEVEL: f32 = 1.0; pub(crate) const DEFAULT_VIDEO_STATUS_POLL_INTERVAL: u64 = 120; @@ -54,75 +57,6 @@ pub fn get_log_prefix() -> String { LOG_PREFIX.to_string() } -/// Settings for a command channel. -// #[derive(Deserialize, Serialize, Debug, Clone, Default)] -// pub struct CommandChannelSettings { -// pub id: ChannelId, -// pub perms: GenericPermissionSettings, -// } - -/// Command channels to restrict where and who can use what commands -#[derive(Deserialize, Serialize, Debug, Clone, Default, PartialEq, sqlx::FromRow)] -pub struct CommandChannels { - pub music_channel: Option, -} - -impl CommandChannels { - /// Set the music channel, mutating. - pub fn set_music_channel( - &mut self, - channel_id: ChannelId, - guild_id: GuildId, - perms: GenericPermissionSettings, - ) -> &mut Self { - self.music_channel = Some(CommandChannel { - command: "music".to_string(), - channel_id, - guild_id, - permission_settings: perms, - }); - self - } - - /// Set the music channel, returning a new CommandChannels. - pub fn with_music_channel( - self, - channel_id: ChannelId, - guild_id: GuildId, - perms: GenericPermissionSettings, - ) -> Self { - let music_channel = Some(CommandChannel { - command: "music".to_string(), - channel_id, - guild_id, - permission_settings: perms, - }); - Self { music_channel } - } - - /// Get the music channel. - pub fn get_music_channel(&self) -> Option { - self.music_channel.clone() - } - - /// Insert the command channel into the database. - pub async fn save(&self, pool: &PgPool) -> Option { - match self.music_channel { - Some(ref c) => c.insert_command_channel(pool).await.ok(), - None => None, - } - } - - pub async fn load(guild_id: GuildId, pool: &PgPool) -> Result { - let music_channels = - CommandChannel::get_command_channels(pool, "music".to_string(), guild_id).await; - - let music_channel = music_channels.first().cloned(); - - Ok(Self { music_channel }) - } -} - #[derive(Default, Deserialize, Serialize, Debug, Clone, PartialEq)] pub struct LogSettings { // TODO: Decide if I want to have separate raw events and all log channels. @@ -362,15 +296,20 @@ impl UserPermission { } } +use super::permissions::GenericPermissionSettings; + +// TODO +//#[derive(Debug, Clone, Serialize, PartialEq)] #[derive(Deserialize, Serialize, Debug, Clone, PartialEq)] pub struct GuildSettings { pub guild_id: GuildId, pub guild_name: String, pub prefix: String, + // A vc token per guild. #[serde(default = "premium_default")] pub premium: bool, - #[serde(default = "CommandChannels::default")] - pub command_channels: CommandChannels, + // Settings for different categories of commands. + pub command_settings: HashMap, #[serde(default = "default_false")] pub autopause: bool, #[serde(default = "default_true")] @@ -470,7 +409,7 @@ impl From for GuildSettings { .into_iter() .map(|x| x as u64) .collect(); - settings.old_volume = settings_db.old_volume as f32; + settings.old_volume = settings_db.volume as f32; settings.volume = settings_db.volume as f32; settings.self_deafen = settings_db.self_deafen; settings.timeout = settings_db.timeout_seconds.unwrap_or(0) as u32; @@ -504,7 +443,7 @@ impl GuildSettings { guild_name, prefix: my_prefix.clone(), premium: DEFAULT_PREMIUM, - command_channels: CommandChannels::default(), + command_settings: HashMap::new(), autopause: false, autoplay: true, reply_with_embed: true, @@ -635,14 +574,20 @@ impl GuildSettings { self.banned_domains = banned; } - /// Set the music channel, without mutating. - pub fn set_music_channel(&mut self, channel_id: u64) -> &mut Self { - self.command_channels.set_music_channel( - ChannelId::new(channel_id), - self.guild_id, - Default::default(), - ); - self + /// Set the music channel, mutating. + pub fn set_music_channel(&mut self, channel_id: u64) { + self.command_settings + .entry("music".to_string()) + .and_modify(|perms| { + perms.allowed_channels.clear(); + perms.denied_channels.clear(); + perms.add_allowed_channel(channel_id); + }) + .or_insert_with(|| { + let mut perms = GenericPermissionSettings::default(); + perms.add_allowed_channel(channel_id); + perms + }); } /// Update the allowed domains. @@ -703,7 +648,7 @@ impl GuildSettings { /// Set the volume level without mutating. pub fn with_volume(self, volume: f32) -> Self { Self { - old_volume: self.volume, + old_volume: volume, volume, ..self } @@ -711,7 +656,7 @@ impl GuildSettings { /// Set the volume level with mutating. pub fn set_volume(&mut self, volume: f32) -> &mut Self { - self.old_volume = self.volume; + self.old_volume = volume; self.volume = volume; self } @@ -902,14 +847,6 @@ impl GuildSettings { } } - /// Set the command channels, notmutating. - pub fn with_command_channels(&self, command_channels: CommandChannels) -> Self { - Self { - command_channels, - ..self.clone() - } - } - /// Set the server join/leave log channel, mutating. pub fn set_join_leave_log_channel(&mut self, channel_id: u64) -> &mut Self { if let Some(log_settings) = &mut self.log_settings { @@ -922,19 +859,40 @@ impl GuildSettings { self } + /// Set command settings, returning a new GuildSettings. + pub fn with_command_settings( + self, + command_settings: HashMap, + ) -> Self { + Self { + command_settings, + ..self + } + } + + /// Set the command settings, mutating. + pub fn set_command_settings( + &mut self, + command_settings: HashMap, + ) -> &mut Self { + self.command_settings = command_settings; + self + } + /// Get the log channel for the given event type. pub fn get_log_channel_type_fe(&self, event: &FullEvent) -> Option { let log_settings = self.log_settings.clone().unwrap_or_default(); match event { - | FullEvent::PresenceUpdate { .. } => { + FullEvent::PresenceUpdate { .. } => { None //.or(log_settings.get_all_log_channel()), - } - | FullEvent::GuildMemberAddition { .. } - | FullEvent::GuildMemberRemoval { .. } => { - log_settings.get_join_leave_log_channel().or(log_settings.get_all_log_channel()) - } - | FullEvent::GuildBanRemoval { .. } + }, + FullEvent::GuildMemberAddition { .. } | FullEvent::GuildMemberRemoval { .. } => { + log_settings + .get_join_leave_log_channel() + .or(log_settings.get_all_log_channel()) + }, + FullEvent::GuildBanRemoval { .. } | FullEvent::GuildBanAddition { .. } | FullEvent::GuildScheduledEventCreate { .. } | FullEvent::GuildScheduledEventUpdate { .. } @@ -951,7 +909,6 @@ impl GuildSettings { | FullEvent::GuildRoleCreate { .. } | FullEvent::GuildRoleDelete { .. } | FullEvent::GuildRoleUpdate { .. } - //| FullEvent::GuildUnavailable { .. } | FullEvent::GuildUpdate { .. } => log_settings .get_server_log_channel() .or(log_settings.get_all_log_channel()), @@ -994,7 +951,6 @@ impl GuildSettings { | FullEvent::ThreadMembersUpdate { .. } | FullEvent::ThreadUpdate { .. } | FullEvent::TypingStart { .. } - // | FullEvent::Unknown { .. } | FullEvent::UserUpdate { .. } | FullEvent::VoiceServerUpdate { .. } | FullEvent::VoiceStateUpdate { .. } => { @@ -1003,8 +959,13 @@ impl GuildSettings { // format!("Event: {:?}", event).as_str().to_string().white() // ); log_settings.get_all_log_channel() - } - _ => todo!(), + }, + //| FullEvent::GuildUnavailable { .. } + // | FullEvent::Unknown { .. } + _ => { + tracing::warn!("Event Not Implemented: {:?}", event); + None + }, } } @@ -1027,6 +988,26 @@ impl GuildSettings { } None } + + pub fn get_music_channel(&self) -> Option { + self.command_settings + .get("music") + .and_then(|x| x.allowed_channels.iter().map(|x| ChannelId::new(*x)).next()) + .or(None) + } + + pub fn get_music_permissions(&self) -> Option { + self.command_settings.get("music").cloned() + } + + /// Adds a user to the denied music users list. + pub async fn add_denied_music_user(&mut self, user_id: UserId) -> CrackedResult { + let user_id = user_id.get(); + let mut perms = self.get_music_permissions().unwrap_or_default(); + perms.add_denied_user(user_id); + self.command_settings.insert("music".to_string(), perms); + Ok(true) + } } /// Save the guild settings to the database. @@ -1039,6 +1020,13 @@ pub async fn save_guild_settings( } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +pub struct CommandSettingsMap; + +impl TypeMapKey for CommandSettingsMap { + type Value = HashMap; +} + /// Struct to hold the guild settings map in a typemap #[derive(Default)] pub struct GuildSettingsMap; diff --git a/crack-core/src/handlers/event_log.rs b/crack-core/src/handlers/event_log.rs index 499ec3fde..1e763dba1 100644 --- a/crack-core/src/handlers/event_log.rs +++ b/crack-core/src/handlers/event_log.rs @@ -1,15 +1,16 @@ use super::event_log_impl::*; use crate::{ errors::CrackedError, guild::settings::GuildSettings, log_event, log_event2, - utils::send_log_embed_thumb, ArcTRwMap, Data, Error, + messaging::interface::send_log_embed_thumb, ArcTRwMap, Data, Error, }; use colored::Colorize; +use poise::serenity_prelude as serenity; use poise::{ serenity_prelude::{ChannelId, FullEvent, GuildId}, FrameworkContext, }; use serde::{ser::SerializeStruct, Serialize}; -use serenity::all::User; +use serenity::User; #[derive(Debug)] pub struct LogEntry { @@ -90,6 +91,8 @@ pub async fn handle_event( data_global: &Data, ) -> Result<(), Error> { // let event_log = Arc::new(&data_global.event_log); + + use crate::db::GuildEntity; let event_log = std::sync::Arc::new(&data_global.event_log_async); let event_name = event_in.snake_case_name(); let guild_settings = &data_global.guild_settings_map; @@ -151,7 +154,7 @@ pub async fn handle_event( event_in, log_data, &guild_id, - ctx, + &ctx, event_log, event_name ) @@ -169,7 +172,9 @@ pub async fn handle_event( .unwrap_or_default(); } + // Should we log bot messages? if new_message.author.bot { + //&& !crate::poise_ext::check_bot_message(ctx, new_message) { return Ok(()); } log_event!( @@ -341,6 +346,30 @@ pub async fn handle_event( }, #[cfg(feature = "cache")] FullEvent::GuildCreate { guild, is_new } => { + if is_new.unwrap_or(false) { + tracing::warn!("New Guild!!! {}", guild.name); + let mut guild_settings: Option = None; + if data_global.database_pool.is_some() { + let (_, new_guild_settings) = GuildEntity::get_or_create( + data_global.database_pool.as_ref().unwrap(), + guild.id.get() as i64, + guild.name.clone(), + data_global + .bot_settings + .prefix + .clone() + .unwrap_or("r!".to_string()), + ) + .await?; + guild_settings = Some(new_guild_settings); + } else { + tracing::error!("No database pool available"); + }; + if guild_settings.is_some() { + let settings = guild_settings.unwrap(); + data_global.insert_guild(guild.id, settings).await?; + } + } log_event!( log_guild_create, guild_settings, @@ -367,9 +396,9 @@ pub async fn handle_event( }, #[cfg(feature = "cache")] FullEvent::GuildDelete { incomplete, full } => { - let log_data = (event_name, incomplete, full); + let log_data = (incomplete, full); log_event!( - log_unimplemented_event, + log_guild_delete_event, guild_settings, event_in, &log_data, @@ -977,6 +1006,24 @@ pub async fn handle_event( FullEvent::VoiceServerUpdate { event } => { event_log.write_log_obj_async(event_name, event).await }, + FullEvent::VoiceChannelStatusUpdate { + old, + status, + id, + guild_id, + } => { + let log_data = (old, status, id, guild_id); + log_event!( + log_voice_channel_status_update, + guild_settings, + event_in, + &log_data, + &guild_id, + &ctx, + event_log, + event_name + ) + }, FullEvent::WebhookUpdate { guild_id, belongs_to_channel_id, diff --git a/crack-core/src/handlers/event_log_impl.rs b/crack-core/src/handlers/event_log_impl.rs index 4b1baa640..e4e07c7c6 100644 --- a/crack-core/src/handlers/event_log_impl.rs +++ b/crack-core/src/handlers/event_log_impl.rs @@ -1,5 +1,5 @@ use super::serenity::voice_state_diff_str; -use crate::{http_utils::get_guild_name, utils::send_log_embed_thumb, Error}; +use crate::{http_utils::get_guild_name, messaging::interface::send_log_embed_thumb, Error}; use colored::Colorize; use serde::Serialize; use serenity::all::{ @@ -8,6 +8,7 @@ use serenity::all::{ GuildScheduledEventUserAddEvent, GuildScheduledEventUserRemoveEvent, Integration, IntegrationId, Interaction, InviteCreateEvent, InviteDeleteEvent, Member, Message, MessageId, MessageUpdateEvent, Presence, Role, RoleId, ScheduledEvent, Sticker, StickerId, + UnavailableGuild, }; use std::{collections::HashMap, sync::Arc}; @@ -390,6 +391,57 @@ pub async fn log_guild_create( .map(|_| ()) } +/// Logs a guild delete event. +#[cfg(not(tarpaulin_include))] +pub async fn log_guild_delete_event( + channel_id: ChannelId, + http: &impl CacheHttp, + log_data: &(&UnavailableGuild, &Option), +) -> Result<(), Error> { + let &(unavailable, full) = log_data; + let guild_name = crate::http_utils::get_guild_name(http, channel_id).await?; + + // FIXME! + // // make sure we have the guild stored or store it + // if guild_settings_map.read().await.get(&guild_id).is_none() { + // let new_settings = + // GuildSettings::new(guild_id, Some(DEFAULT_PREFIX), Some(guild_name.clone())); + // guild_settings_map + // .write() + // .await + // .insert(guild_id, new_settings.clone()); + // } + + let title = format!("Guild Delete: {}", guild_name); + let mut description = if !unavailable.unavailable { + "Bot was removed from the guild." + } else { + "Guild was deleted." + } + .to_string(); + + if full.is_some() { + let guild: &Guild = full.as_ref().unwrap(); + description = format!( + "Guild was deleted.\nGuild Name: {}\nGuild ID: {}\nGuild Owner: {}", + guild.name, guild.id, guild.owner_id + ); + } + let id = unavailable.id.to_string(); + let avatar_url = ""; + send_log_embed_thumb( + &guild_name, + &channel_id, + http, + &id, + &title, + &description, + avatar_url, + ) + .await + .map(|_| ()) +} + /// Logs a guild role cteate event. #[cfg(not(tarpaulin_include))] pub async fn log_guild_role_create( @@ -1177,6 +1229,31 @@ pub async fn log_presence_update( .await } +/// Log a voice state update event. +#[cfg(not(tarpaulin_include))] +pub async fn log_voice_channel_status_update( + channel_id: ChannelId, + ctx: &SerenityContext, + log_data: &(&Option, &Option, &ChannelId, &GuildId), +) -> Result { + let &(old, status, _, _) = log_data; + let title = format!("Voice Channel Status Update: {:?} -> {:?}", old, status); + + let description = ""; + let avatar_url = ""; + let guild_name = get_guild_name(&ctx, channel_id).await?; + send_log_embed_thumb( + &guild_name, + &channel_id, + &ctx, + &channel_id.to_string(), + &title, + description, + avatar_url, + ) + .await +} + /// Log a voice state update event. #[cfg(not(tarpaulin_include))] pub async fn log_voice_state_update( diff --git a/crack-core/src/handlers/idle.rs b/crack-core/src/handlers/idle.rs index e9395f864..9d22a2f8b 100644 --- a/crack-core/src/handlers/idle.rs +++ b/crack-core/src/handlers/idle.rs @@ -32,7 +32,7 @@ impl EventHandler for IdleHandler { // guaranteed to be the first track in the actual queue, so search the entire list let bot_is_playing = track_list .iter() - .any(|track| matches!(track.0.playing, PlayMode::Play)); + .any(|&(track_state, _track_handle)| matches!(track_state.playing, PlayMode::Play)); // if there's a track playing, then reset the counter if bot_is_playing { diff --git a/crack-core/src/handlers/serenity.rs b/crack-core/src/handlers/serenity.rs index fe6145e21..ac953fa6c 100644 --- a/crack-core/src/handlers/serenity.rs +++ b/crack-core/src/handlers/serenity.rs @@ -13,6 +13,7 @@ use ::serenity::{ }; use chrono::{DateTime, Utc}; use colored::Colorize; +// use dashmap; use poise::serenity_prelude::{self as serenity, Error as SerenityError, Member, Mentionable}; use serenity::{ async_trait, @@ -134,65 +135,6 @@ impl EventHandler for SerenityHandler { } } - /* - async fn message(&self, ctx: SerenityContext, msg: serenity::Message) { - struct MyMessage(serenity::Message); - impl fmt::Display for MyMessage { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut result = String::new(); - let msg = &self.0; - // let guild_id = match msg.guild_id { - // Some(guild_id) => guild_id, - // None => { - // tracing::warn!("Non-gateway message received: {:?}", msg); - // GuildId(0) - // } - // }; - let name = msg.author.name.clone(); - let content = msg.content.clone(); - result.push_str(&format!("Message: {} {}", name.purple(), content.purple(),)); - msg.embeds.iter().for_each(|x| { - result.push_str(&format!( - "{}{}{}", - x.title.as_ref().unwrap_or(&String::new()).purple(), - x.description.as_ref().unwrap_or(&String::new()).purple(), - x.fields.iter().fold(String::new(), |acc, x| { - format!("{}{}{}", acc, x.name.purple(), x.value.purple()) - }) - )); - }); - write!(f, "{}", result) - } - } - - let guild_id = match msg.guild_id { - Some(guild_id) => guild_id, - None => { - tracing::warn!("Non-gateway message received: {:?}", msg); - return; - } - }; - - let guild_name = { - let guild = guild_id.to_guild_cached(&ctx.cache).unwrap(); - guild.name.clone() - }; - let name = msg.author.name.clone(); - // let guild_name = guild.name; - let content = msg.content.clone(); - let channel_name = msg.channel_id.name(&ctx.clone()).await.unwrap_or_default(); - - tracing::info!( - "Message: {} {} {} {}", - name.purple(), - guild_name.purple(), - channel_name.purple(), - content.purple(), - ); - let _mm = MyMessage(msg); - } - */ - async fn voice_state_update( &self, ctx: SerenityContext, @@ -261,7 +203,6 @@ impl EventHandler for SerenityHandler { let config = self.data.bot_settings.clone(); let video_status_poll_interval = config.get_video_status_poll_interval(); - // let config.get // it's safe to clone Context, but Arc is cheaper for this use case. // Untested claim, just theoretically. :P let arc_ctx = Arc::new(ctx.clone()); @@ -565,16 +506,8 @@ async fn check_delete_old_messages( let mut to_delete = Vec::::new(); for guild_id in guild_ids.iter() { tracing::warn!("Checking guild {}", guild_id); - data.guild_msg_cache_ordered - .lock() - .unwrap() - .get_mut(guild_id); - if let Some(guild_cache) = data - .guild_msg_cache_ordered - .lock() - .unwrap() - .get_mut(guild_id) - { + data.guild_msg_cache_ordered.lock().await.get_mut(guild_id); + if let Some(guild_cache) = data.guild_msg_cache_ordered.lock().await.get_mut(guild_id) { let now = DateTime::::from(SystemTime::now()); for (creat_time, msg) in guild_cache.time_ordered_messages.iter() { let delta = now.signed_duration_since(*creat_time); @@ -596,36 +529,6 @@ async fn check_delete_old_messages( } Ok(()) } -// #[allow(dead_code)] -// async fn disconnect_member( -// ctx: Arc, -// cam: CamPollEvent, -// guild: GuildId, -// ) -> Result { -// guild -// .edit_member(&ctx, cam.user_id, EditMember::default().disconnect_member()) -// .await -// } - -// async fn server_defeafen_member( -// ctx: Arc, -// cam: CamPollEvent, -// guild: GuildId, -// ) -> Result { -// guild -// .edit_member(&ctx, cam.user_id, EditMember::default().deafen(true)) -// .await -// } - -// async fn server_mute_member( -// ctx: Arc, -// cam: CamPollEvent, -// guild: GuildId, -// ) -> Result { -// guild -// .edit_member(&ctx, cam.user_id, EditMember::default().mute(true)) -// .await -// } /// Returns a string describing the difference between two voice states. pub async fn voice_state_diff_str( @@ -787,3 +690,95 @@ pub async fn voice_state_diff_str( } Ok(result) } + +// /// `ForwardBotTestCommandsHandler` is a handler to check for bot test commands +// /// for cracktunes and forward them to the bot despite being from another bot. +// pub struct ForwardBotTestCommandsHandler<'a> { +// pub poise_ctx: crate::Context<'a>, +// pub options: poise::FrameworkOptions<(), Error>, +// pub cmd_lookup: dashmap::DashMap, +// pub shard_manager: std::sync::Mutex>>, +// } + +// // use serenity::model::channel::Message; + +// #[serenity::async_trait] +// impl serenity::EventHandler for ForwardBotTestCommandsHandler<'_> { +// async fn message(&self, _ctx: SerenityContext, new_message: Message) { +// let allowed_bot_ids = vec![ +// serenity::UserId::new(1111844110597374042), +// serenity::UserId::new(1124707756750934159), +// serenity::UserId::new(1115229568006103122), +// ]; +// if !new_message.author.bot { +// return; +// } +// if !allowed_bot_ids.contains(&new_message.author.id) { +// tracing::error!("Not an allowed bot id"); +// return; +// } +// let id = new_message.author.id; +// // let guard = ctx.data.read().await; +// // let prefix = match guard.get::() { +// // Some(map) => map +// // .get(&new_message.guild_id.unwrap()) +// // .map(|x| x.prefix.clone()), +// // _ => None, +// // }; +// tracing::error!("Allowing bot id {:} to run command...", id); +// let opt_cmd = parse_command(new_message.content.clone()); +// tracing::error!("opt_cmd: {:?}", opt_cmd); +// if !opt_cmd.is_some() { +// tracing::error!("BYE"); +// return; +// } +// tracing::error!("HERE"); +// let cmd = opt_cmd.unwrap(); +// let _ = execute_command_or_err(self.poise_ctx, cmd).await; +// } +// } + +// fn parse_command(content: String) -> Option { +// content +// .clone() +// .split_whitespace() +// .next() +// .map(|cmd| cmd[1..].to_string()) +// } + +// async fn execute_command_or_err(ctx: crate::Context<'_>, command: String) -> CommandResult { +// poise::extract_command_and_run_checks(framework, ctx, interaction, interaction_type, has_sent_initial_response, invocation_data, options, parent_commands) +// match command.as_str() { +// "ping" => crate::commands::ping_internal(ctx).await, +// _ => return Err(Box::new(CrackedError::CommandNotFound(command))), +// } +// // cmd.create_as_slash_command() +// // .unwrap() +// // .execute(cache_http, ctx) +// // .await +// // .map_err(|err| err.into()) +// // .map(|_| ()) +// } + +// #[cfg(test)] +// mod test { +// use super::parse_command; + +// #[test] +// fn test_parse_command() { +// let command_str = "~ping".to_string(); +// let want = "ping".to_string(); +// let got = parse_command(command_str).unwrap(); + +// assert_eq!(want, got); +// } + +// #[test] +// fn test_parse_command_two() { +// let command_str = "!play lalalalal alla".to_string(); +// let want = "play".to_string(); +// let got = parse_command(command_str).unwrap(); + +// assert_eq!(want, got); +// } +// } diff --git a/crack-core/src/handlers/track_end.rs b/crack-core/src/handlers/track_end.rs index 4a3456b6c..31fa9d2b8 100644 --- a/crack-core/src/handlers/track_end.rs +++ b/crack-core/src/handlers/track_end.rs @@ -1,18 +1,8 @@ -use ::serenity::{ - all::{Cache, ChannelId, UserId}, - async_trait, - builder::EditMessage, - http::Http, - model::id::GuildId, -}; -use serenity::all::CacheHttp; -use songbird::{input::AuxMetadata, tracks::TrackHandle, Call, Event, EventContext, EventHandler}; -use std::{sync::Arc, time::Duration}; -use tokio::sync::Mutex; - +use crate::commands::play_utils::queue_track_ready_front; +use crate::commands::play_utils::ready_query2; use crate::{ - commands::{forget_skip_votes, play_utils::enqueue_track_pgwrite_asdf, MyAuxMetadata}, - db::PlayLog, + commands::{forget_skip_votes, play_utils::QueryType, MyAuxMetadata}, + db::PgPoolExtPlayLog, errors::{verify, CrackedError}, guild::operations::GuildSettingsOperations, messaging::{ @@ -21,8 +11,20 @@ use crate::{ }, sources::spotify::{Spotify, SPOTIFY}, utils::{calculate_num_pages, forget_queue_message, send_now_playing}, - Data, Error, + CrackedResult, + Data, //, Error, +}; +use ::serenity::{ + all::{Cache, ChannelId}, + async_trait, + builder::EditMessage, + http::Http, + model::id::GuildId, }; +use serenity::all::CacheHttp; +use songbird::{tracks::TrackHandle, Call, Event, EventContext, EventHandler}; +use std::sync::Arc; +use tokio::sync::Mutex; /// Handler for the end of a track event. // This needs enough context to be able to send messages to the appropriate @@ -43,7 +45,6 @@ pub struct ModifyQueueHandler { pub cache: Arc, pub call: Arc>, } - /// Event handler to handle the end of a track. #[async_trait] impl EventHandler for TrackEndHandler { @@ -53,7 +54,7 @@ impl EventHandler for TrackEndHandler { tracing::error!("Autoplay: {}", autoplay); - let (autopause, volume) = { + let (autopause, _volume) = { let settings = self.data.guild_settings_map.read().await.clone(); let autopause = settings .get(&self.guild_id) @@ -68,177 +69,85 @@ impl EventHandler for TrackEndHandler { (autopause, volume) }; - self.call.lock().await.queue().modify_queue(|v| { - if let Some(track) = v.front_mut() { - let _ = track.set_volume(volume); - }; - }); - tracing::error!("Set volume"); - if autopause { - tracing::error!("Pausing"); + tracing::trace!("Pausing"); self.call.lock().await.queue().pause().ok(); } else { - tracing::error!("Not pausing"); + tracing::trace!("Not pausing"); } - tracing::error!("Forgetting skip votes"); + tracing::trace!("Forgetting skip votes"); // FIXME match forget_skip_votes(&self.data, self.guild_id).await { - Ok(_) => tracing::warn!("Forgot skip votes"), + Ok(_) => tracing::trace!("Forgot skip votes"), Err(e) => tracing::warn!("Error forgetting skip votes: {}", e), }; let music_channel = self.data.get_music_channel(self.guild_id).await; - let (chan_id, _chan_name, MyAuxMetadata::Data(metadata), cur_position) = { - let (sb_chan_id, my_metadata, cur_pos) = { - let (channel, track) = { - let handler = self.call.lock().await; - let channel = match music_channel { - Some(c) => c, - _ => handler - .current_channel() - .map(|c| ChannelId::new(c.0.get())) - .unwrap(), - }; - let track = handler.queue().current().clone(); - (channel, track) - }; - let chan_id = channel; - // let chan_id = channel.map(|c| ChannelId::new(c.0.get())).unwrap(); - match (track, autoplay) { - (None, false) => ( - channel, - MyAuxMetadata::Data(AuxMetadata::default()), - Duration::from_secs(0), - ), - (None, true) => { - let spotify = SPOTIFY.lock().await; - let spotify = - verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED)) - .unwrap(); - // Get last played tracks from the db - let last_played = PlayLog::get_last_played( - self.data.database_pool.as_ref().unwrap(), - None, - Some(self.guild_id.get() as i64), - ) - .await - .unwrap_or_default(); - let res_rec = - Spotify::get_recommendations(spotify, last_played.clone()).await; - let (rec, msg) = match res_rec { - Ok(rec) => { - // let msg0 = format!( - // "Previously played: \n{}", - // last_played.clone().join("\n") - // ); - let msg1 = - format!("Autoplaying (/autoplay or /stop to stop): {}", rec[0]); - (rec, msg1) - }, - Err(e) => { - let msg = format!("Error: {}", e); - let rec = vec![]; - (rec, msg) - }, - }; - tracing::warn!("{}", msg); - let msg = chan_id - .say((&self.cache, self.http.as_ref()), msg) - .await - .unwrap(); - self.data.add_msg_to_cache(self.guild_id, msg); - let query = match Spotify::search(spotify, &rec[0]).await { - Ok(query) => query, - Err(e) => { - let msg = format!("Error: {}", e); - tracing::warn!("{}", msg); - // chan_id.say(&self.http, msg).await.unwrap(); - return None; - }, - }; - let cache_http = (&self.cache, self.http.as_ref()); - let tracks = enqueue_track_pgwrite_asdf( - self.data.database_pool.as_ref().unwrap(), - self.guild_id, - chan_id, - UserId::new(1), - cache_http, - &self.call, - &query, - ) - .await - .unwrap_or_default(); - let (my_metadata, pos) = match tracks.first() { - Some(t) => { - let (my_metadata, pos) = - extract_track_metadata(t).await.unwrap_or_default(); - let _ = t.set_volume(volume); - (my_metadata, pos) - }, - None => { - let msg = format!("No tracks found for query: {:?}", query); - tracing::warn!("{}", msg); - ( - MyAuxMetadata::Data(AuxMetadata::default()), - Duration::from_secs(0), - ) - }, - }; + if !autoplay { + return None; + } - (channel, my_metadata, pos) - }, - (Some(track), _) => { - let _ = track.set_volume(volume); - let (my_metadata, pos) = - extract_track_metadata(&track).await.unwrap_or_default(); + let pool = if let Some(pool) = &self.data.database_pool { + pool + } else { + return None; + }; - (channel, my_metadata, pos) - }, - } + let (channel, next_track) = { + let handler = self.call.lock().await; + let channel = match music_channel { + Some(c) => c, + _ => handler + .current_channel() + .map(|c| ChannelId::new(c.0.get())) + .unwrap(), }; - // let chan_id = sb_chan_id.map(|id| ChannelId::new(id.0.get())).unwrap(); - let chan_id = sb_chan_id; - let chan_name = chan_id.name(&self.http).await.unwrap(); - (chan_id, chan_name, my_metadata, cur_pos) + let track = handler.queue().current().clone(); + (channel, track) }; - tracing::warn!("Sending now playing message"); + if next_track.is_some() { + return None; + } + + let query = match get_recommended_track_query(pool, self.guild_id).await { + Ok(query) => query, + Err(e) => { + let msg = format!("Error: {}", e); + tracing::warn!("{}", msg); + return None; + }, + }; + let track_ready = ready_query2(query).await.ok()?; + let MyAuxMetadata::Data(metadata) = &track_ready.metadata; + let metadata = Some(metadata.clone()); + + let track = queue_track_ready_front(&self.call, track_ready) + .await + .ok()?; + + let chan_id = channel; + let track_state = track.first().as_ref()?.get_info().await; + let cur_position = track_state.map(|x| x.position).ok(); match send_now_playing( chan_id, self.http.clone(), self.call.clone(), - Some(cur_position), - Some(metadata), + cur_position, + metadata, ) .await { - Ok(message) => { - self.data.add_msg_to_cache(self.guild_id, message); - tracing::info!("Sent now playing message"); - }, + Ok(_) => tracing::trace!("Sent now playing message"), Err(e) => tracing::warn!("Error sending now playing message: {}", e), - } - + }; None } } -/// Extracts the metadata and position of a track. -async fn extract_track_metadata(track: &TrackHandle) -> Result<(MyAuxMetadata, Duration), Error> { - let pos = track.get_info().await?.position; - let track_clone = track.clone(); - let mutex_guard = track_clone.typemap().read().await; - let my_metadata = mutex_guard - .get::() - .ok_or_else(|| CrackedError::Other("No metadata found"))? - .clone(); - Ok((my_metadata, pos)) -} - /// Event handler to set the volume of the playing track to the volume /// set in the guild settings after a queue modification. #[async_trait] @@ -299,3 +208,25 @@ pub async fn update_queue_messages( }; } } + +/// Get's the recommended tracks for a guild. Returns `QueryType::None` on failure. +/// Looks at the top +async fn get_recommended_track_query( + pool: &sqlx::PgPool, + guild_id: GuildId, +) -> CrackedResult { + let spotify = SPOTIFY.lock().await; + let spotify = verify(spotify.as_ref(), CrackedError::Other(SPOTIFY_AUTH_FAILED))?; + + let last_played = pool.get_last_played_by_guild(guild_id, 5).await?; + let res_rec = Spotify::get_recommendations(spotify, last_played.clone()).await?; + + if res_rec.is_empty() { + return Ok(QueryType::None); + } + + match Spotify::search(spotify, &res_rec[0]).await { + Ok(query) => Ok(query), + Err(e) => Err(e), + } +} diff --git a/crack-core/src/handlers/voice.rs b/crack-core/src/handlers/voice.rs index 26e282170..12c5ffb45 100644 --- a/crack-core/src/handlers/voice.rs +++ b/crack-core/src/handlers/voice.rs @@ -3,6 +3,7 @@ use serenity::async_trait; use serenity::client::EventHandler; use serenity::prelude::RwLock; use serenity::{client::Context as SerenityContext, model::gateway::Ready}; +use songbird::tracks::PlayMode; use songbird::{ model::payload::{ClientDisconnect, Speaking}, Event, EventContext, EventHandler as VoiceEventHandler, @@ -163,11 +164,40 @@ impl VoiceEventHandler for Receiver { } tracing::warn!("{:?}", track_data); for &(track_state, track_handle) in track_data.iter() { - tracing::warn!( - "Track started: {:?} (handle: {:?})", - track_state, - track_handle, - ); + match track_state.playing { + PlayMode::Play => { + tracing::warn!( + "Track started: {:?} (handle: {:?})", + track_state, + track_handle, + ); + }, + PlayMode::Pause => { + tracing::warn!( + "Track paused: {:?} (handle: {:?})", + track_state, + track_handle, + ); + }, + PlayMode::Stop | PlayMode::End => { + tracing::warn!( + "Track ended: {:?} (handle: {:?})", + track_state, + track_handle, + ); + }, + PlayMode::Errored(_) => { + tracing::warn!( + "Track errored: {:?} (handle: {:?})", + track_state, + track_handle, + ); + }, + _ => { + // There is a new variant of PlayMode that is not handled. + unimplemented!() + }, + } } }, Ctx::VoiceTick(_) @@ -189,13 +219,14 @@ impl VoiceEventHandler for Receiver { /// Registers the voice handlers for a call instance for the bot. /// These are kept per guild. +/// FIXME: This seems to be called too many times? pub async fn register_voice_handlers( buffer: Arc>>, - handler_lock: Arc>, + call: Arc>, ctx: SerenityContext, ) -> Result<(), CrackedError> { // NOTE: this skips listening for the actual connection result. - let mut handler = handler_lock.lock().await; + let mut handler = call.lock().await; // allocating memory, need to drop this when y??? handler.add_global_event( diff --git a/crack-core/src/handlers/voice_chat_stats.rs b/crack-core/src/handlers/voice_chat_stats.rs index 99860e086..a8a102e91 100644 --- a/crack-core/src/handlers/voice_chat_stats.rs +++ b/crack-core/src/handlers/voice_chat_stats.rs @@ -1,18 +1,27 @@ use crate::{ commands::{deafen_internal, mute_internal}, errors::CrackedError, + messaging::messages::UNKNOWN, BotConfig, CamKickConfig, }; -use ::serenity::builder::CreateMessage; +use poise::serenity_prelude as serenity; + +use ::serenity::all::CacheHttp; use colored::Colorize; -use poise::serenity_prelude::{self as serenity, Channel, Mentionable, UserId}; -use serenity::{model::id::GuildId, ChannelId, Context as SerenityContext}; +use serenity::{ + builder::CreateMessage, model::id::GuildId, Channel, ChannelId, Context as SerenityContext, + Mentionable, UserId, VoiceState, +}; use std::{ cmp::{Eq, PartialEq}, collections::{HashMap, HashSet}, sync::Arc, }; -use tokio::time::{Duration, Instant}; +use tokio::{ + task::JoinHandle, + time::{Duration, Instant}, +}; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Enum for the Camera status. enum CamStatus { @@ -65,7 +74,8 @@ async fn check_and_enforce_cams( cam_states: &mut HashMap<(UserId, ChannelId), CamPollEvent>, config_map: &HashMap, //status_changes: &mut Vec, - ctx: Arc, + //ctx: Arc, + cache_http: &impl CacheHttp, ) -> Result<(), CrackedError> { let kick_conf = config_map .get(&cur_cam.chan_id.get()) @@ -88,7 +98,7 @@ async fn check_and_enforce_cams( if cur_cam.status == CamStatus::Off && cur_cam.last_change.elapsed() > Duration::from_secs(kick_conf.timeout) { - let user = match new_cam.user_id.to_user(&ctx).await { + let user = match new_cam.user_id.to_user(cache_http).await { Ok(user) => user, Err(err) => { tracing::error!("Error getting user: {err}"); @@ -106,7 +116,8 @@ async fn check_and_enforce_cams( tracing::info!("about to deafen {:?}", new_cam.user_id); if false { - run_cam_enforcement(ctx, new_cam, guild_id, user, kick_conf, cam_states).await; + run_cam_enforcement(cache_http, new_cam, guild_id, user, kick_conf, cam_states) + .await; } } }; @@ -115,7 +126,8 @@ async fn check_and_enforce_cams( /// Run the camera enforcement rules. async fn run_cam_enforcement( - ctx: Arc, + //ctx: Arc, + cache_http: &impl CacheHttp, new_cam: &CamPollEvent, guild_id: GuildId, user: ::serenity::model::prelude::User, @@ -126,11 +138,11 @@ async fn run_cam_enforcement( // FIXME: Should this not be it's own function? // let dc_res = disconnect_member(ctx.clone(), *cam, guild).await; let dc_res1 = ( - deafen_internal(ctx.clone(), guild_id, user.clone(), true).await, + deafen_internal(cache_http, guild_id, user.clone(), true).await, "deafen", ); let dc_res2 = ( - mute_internal(ctx.clone(), user.clone(), guild_id, true).await, + mute_internal(cache_http, user.clone(), guild_id, true).await, "deafen", ); // let dc_res1 = ( @@ -153,7 +165,7 @@ async fn run_cam_enforcement( let channel = ChannelId::new(kick_conf.chan_id); let _ = channel .send_message( - &ctx, + cache_http, CreateMessage::default().content({ format!("{} {}: {}", user.mention(), kick_conf.dc_msg, state) }), @@ -175,34 +187,32 @@ async fn check_camera_status( ctx: Arc, guild_id: GuildId, ) -> (Vec, String) { - let (voice_states, guild_name) = match guild_id.to_guild_cached(&ctx) { - Some(guild) => (guild.voice_states.clone(), guild.name.clone()), - // Err(err) => { - // tracing::error!("{err}"); - None => { - // let partial_guild = ctx.http().get_guild(guild_id).await.unwrap(); - tracing::error!("Guild not found {guild_id}."); - return (vec![], "".to_string()); - }, - }; + let (voice_states, guild_name): (HashMap, String) = + match guild_id.to_guild_cached(&ctx) { + Some(guild) => (guild.voice_states.clone(), guild.name.clone()), + None => { + tracing::error!("Guild not found {guild_id}."); + return (vec![], "".to_string()); + }, + }; let mut cams = Vec::new(); let mut output: String = format!("{}\n", guild_name.bright_green()); for (user_id, voice_state) in voice_states { if let Some(chan_id) = voice_state.channel_id { - let user = match user_id.to_user(&ctx).await { - Ok(user) => user, + let user_name = match user_id.to_user(&ctx).await { + Ok(user) => user.name, Err(err) => { tracing::error!("Error getting user: {err}"); - continue; + UNKNOWN.to_string() }, }; let channel_name = match chan_id.to_channel(&ctx).await { Ok(chan) => match chan { Channel::Guild(chan) => chan.name, Channel::Private(chan) => chan.name(), - _ => String::from("Unknown"), + _ => String::from(UNKNOWN), }, Err(err) => { tracing::error!( @@ -226,11 +236,10 @@ async fn check_camera_status( cams.push(info); output.push_str(&format!( "{}|{}|{}|{}|{}|{}\n", - guild_name, &user.name, &user.id, &channel_name, &chan_id, status, + guild_name, &user_name, &user_id, &channel_name, &chan_id, status, )); } } - // tracing::warn!("{}", output.bright_cyan()); (cams, output) } @@ -239,7 +248,7 @@ pub async fn cam_status_loop( ctx: Arc, config: Arc, guilds: Vec, -) { +) -> JoinHandle<()> { tokio::spawn(async move { tracing::info!("Starting camera status check loop"); let configs = config.cam_kick.clone().unwrap_or_default(); @@ -277,15 +286,9 @@ pub async fn cam_status_loop( for new_cam in new_cams.iter_mut() { if let Some(status) = cur_cams.get(&new_cam.key()) { - let _ = check_and_enforce_cams( - *status, - new_cam, - &mut cur_cams, - &channels, - //&mut status_changes, - Arc::clone(&ctx), - ) - .await; + let _ = + check_and_enforce_cams(*status, new_cam, &mut cur_cams, &channels, &ctx) + .await; } else { cur_cams.insert(new_cam.key(), **new_cam); } @@ -303,5 +306,94 @@ pub async fn cam_status_loop( ); tokio::time::sleep(Duration::from_secs(config.get_video_status_poll_interval())).await; } - }); + }) +} + +#[cfg(test)] +mod test { + // Test CamStatus enum + use super::*; + + #[test] + fn test_cam_status() { + let on = CamStatus::On; + let off = CamStatus::Off; + assert_eq!(on, CamStatus::On); + assert_eq!(off, CamStatus::Off); + } + + #[test] + fn test_cam_status_display() { + let on = CamStatus::On; + let off = CamStatus::Off; + assert_eq!(format!("{}", on), "On"); + assert_eq!(format!("{}", off), "Off"); + } + + #[test] + fn test_cam_status_from_bool() { + let on = CamStatus::from(true); + let off = CamStatus::from(false); + assert_eq!(on, CamStatus::On); + assert_eq!(off, CamStatus::Off); + } + + // CamPollEvent tests + #[test] + fn test_cam_poll_event_key() { + let user_id = UserId::new(123); + let chan_id = ChannelId::new(456); + let cam = CamPollEvent { + user_id, + guild_id: GuildId::new(789), + chan_id, + status: CamStatus::On, + last_change: Instant::now(), + }; + assert_eq!(cam.key(), (user_id, chan_id)); + } + + #[tokio::test] + async fn test_check_and_enforce_cams() { + let user_id = UserId::new(123); + let chan_id = ChannelId::new(456); + let cam = CamPollEvent { + user_id, + guild_id: GuildId::new(789), + chan_id, + status: CamStatus::On, + last_change: Instant::now(), + }; + let mut cam_states = HashMap::<(UserId, ChannelId), CamPollEvent>::new(); + let config_map = HashMap::::new(); + let http = poise::serenity_prelude::http::Http::new(""); + let cache = Arc::new(poise::serenity_prelude::Cache::new()); + let cache_http = (&cache, &http); + // let ctx = Arc::new(SerenityContext::new()); + let res = + check_and_enforce_cams(cam, &cam, &mut cam_states, &config_map, &cache_http).await; + let want = CrackedError::Other("Channel not found"); + assert_eq!(res, Err(want)); + } + + // fn new_serenity_context() -> Arc { + // let token = std::env::var("DISCORD_BOT_TOKEN")?; + // let shard_info = ShardInfo { + // id: ShardId(0), + // total: 1, + // }; + + // // retrieve the gateway response, which contains the URL to connect to + // let gateway = Arc::new(Mutex::new(http.get_gateway().await?.url)); + // let shard = Shard::new(gateway, &token, shard_info, GatewayIntents::all(), None).await?; + // Arc::new(SerenityContext { + // data: Arc::new(tokio::sync::RwLock::new( + // poise::serenity_prelude::prelude::TypeMap::new(), + // )), + // http: Arc::new(poise::serenity_prelude::http::Http::new("")), + // shard: poise::serenity_prelude::Shard::new("".to_string(), "".to_string()), + // cache: Arc::new(poise::serenity_prelude::Cache::new()), + // shard_id: poise::serenity_prelude::ShardId(0), + // }) + // } } diff --git a/crack-core/src/http_utils.rs b/crack-core/src/http_utils.rs index e1c5888e6..f4586e01d 100644 --- a/crack-core/src/http_utils.rs +++ b/crack-core/src/http_utils.rs @@ -3,18 +3,79 @@ use reqwest::Client; use std::future::Future; use crate::errors::CrackedError; -use crate::messaging::message::CrackedMessage; +use crate::messaging::{message::CrackedMessage, messages::UNKNOWN}; +use crate::serenity::Color; use serenity::all::{ CacheHttp, ChannelId, CreateEmbed, CreateMessage, GuildId, Http, Message, UserId, }; /// Parameter structure for functions that send messages to a channel. +#[derive(Debug, PartialEq)] pub struct SendMessageParams { pub channel: ChannelId, pub as_embed: bool, pub ephemeral: bool, pub reply: bool, + pub color: Color, + pub cache_msg: bool, pub msg: CrackedMessage, + pub embed: Option, +} + +impl Default for SendMessageParams { + fn default() -> Self { + SendMessageParams { + channel: ChannelId::new(1), + as_embed: true, + ephemeral: false, + reply: true, + color: Color::BLUE, + cache_msg: true, + msg: CrackedMessage::Other(String::new()), + embed: None, + } + } +} + +impl SendMessageParams { + pub fn new(msg: CrackedMessage) -> Self { + Self { + msg, + ..Default::default() + } + } + + pub fn with_as_embed(self, as_embed: bool) -> Self { + Self { as_embed, ..self } + } + + pub fn with_ephemeral(self, ephemeral: bool) -> Self { + Self { ephemeral, ..self } + } + + pub fn with_reply(self, reply: bool) -> Self { + Self { reply, ..self } + } + + pub fn with_color(self, color: Color) -> Self { + Self { color, ..self } + } + + pub fn with_msg(self, msg: CrackedMessage) -> Self { + Self { msg, ..self } + } + + pub fn with_channel(self, channel: ChannelId) -> Self { + Self { channel, ..self } + } + + pub fn with_cache_msg(self, cache_msg: bool) -> Self { + Self { cache_msg, ..self } + } + + pub fn with_embed(self, embed: Option) -> Self { + Self { embed, ..self } + } } /// Extension trait for CacheHttp to add some utility functions. @@ -31,6 +92,10 @@ pub trait CacheHttpExt { &self, params: SendMessageParams, ) -> impl Future> + Send; + fn guild_name_from_guild_id( + &self, + guild_id: GuildId, + ) -> impl Future> + Send; } /// Implement the CacheHttpExt trait for any type that implements CacheHttp. @@ -74,12 +139,15 @@ impl CacheHttpExt for T { }; channel.send_message(self, msg).await.map_err(Into::into) } + + async fn guild_name_from_guild_id(&self, guild_id: GuildId) -> Result { + guild_name_from_guild_id(self, guild_id).await + } } /// This is a hack to get around the fact that we can't use async in statics. Is it? static CLIENT: Lazy = Lazy::new(|| { println!("Creating a new reqwest client..."); - tracing::info!("Creating a new reqwest client..."); reqwest::ClientBuilder::new() .use_rustls_tls() .build() @@ -124,17 +192,18 @@ pub async fn get_bot_id(cache_http: impl CacheHttp) -> Result String { // let asdf = cache.cache()?.user(user_id); + match cache_http.cache() { Some(cache) => match cache.user(user_id) { Some(x) => x.name.clone(), None => { tracing::warn!("cache.user returned None"); - "Unknown".to_string() + UNKNOWN.to_string() }, }, None => { tracing::warn!("cache_http.cache() returned None"); - "Unknown".to_string() + UNKNOWN.to_string() }, } } @@ -193,7 +262,6 @@ mod test { let url = "https://example.com"; let final_url = resolve_final_url(url).await.unwrap(); - // assert_eq!(final_url, "https://example.com/"); assert_eq!(final_url, "https://example.com/"); } } diff --git a/crack-core/src/lib.rs b/crack-core/src/lib.rs index 6b6fb0d97..06c8adaff 100644 --- a/crack-core/src/lib.rs +++ b/crack-core/src/lib.rs @@ -1,7 +1,6 @@ +#![feature(linked_list_cursors)] use crate::handlers::event_log::LogEntry; use chrono::{DateTime, Utc}; -use commands::play_utils::TrackReadyData; -use commands::MyAuxMetadata; #[cfg(feature = "crack-gpt")] use crack_gpt::GptContext; use db::worker_pool::MetadataMsg; @@ -13,47 +12,70 @@ use guild::settings::{ DEFAULT_DB_URL, DEFAULT_LOG_PREFIX, DEFAULT_PREFIX, DEFAULT_VIDEO_STATUS_POLL_INTERVAL, DEFAULT_VOLUME_LEVEL, }; +use poise::serenity_prelude as serenity; use serde::{Deserialize, Serialize}; -use serenity::all::{ChannelId, GuildId, Message}; -use songbird::Call; +use serenity::all::{GuildId, Message, UserId}; +use std::time::SystemTime; use std::{ collections::{BTreeMap, HashMap, HashSet}, fmt::Display, fs, fs::File, - future::Future, io::Write, path::Path, - sync::{Arc, Mutex as SyncMutex, RwLock as SyncRwLock}, + sync::Arc, }; use tokio::sync::{mpsc::Sender, Mutex, RwLock}; pub mod commands; +pub mod config; pub mod connection; pub mod db; pub mod errors; pub mod guild; pub mod handlers; pub mod http_utils; +#[macro_use] +pub mod macros; pub mod messaging; pub mod metrics; +pub mod poise_ext; pub mod sources; #[cfg(test)] pub mod test; pub mod utils; +// ------------------------------------------------------------------ +// Public types we use to simplify return and parameter types. +// ------------------------------------------------------------------ + pub type Error = Box; -pub type Context<'a> = poise::Context<'a, Data, Error>; pub type ArcTRwLock = Arc>; pub type ArcTMutex = Arc>; pub type ArcRwMap = Arc>>; pub type ArcTRwMap = Arc>>; pub type ArcMutDMap = Arc>>; pub type CrackedResult = std::result::Result; +pub type CrackedResult2 = anyhow::Result; + +pub type Command = poise::Command; +pub type Context<'a> = poise::Context<'a, Data, CommandError>; +pub type PrefixContext<'a> = poise::PrefixContext<'a, Data, CommandError>; +pub type PartialContext<'a> = poise::PartialContext<'a, Data, CommandError>; +pub type ApplicationContext<'a> = poise::ApplicationContext<'a, Data, CommandError>; + +pub type CommandError = Error; +pub type CommandResult = Result<(), E>; +pub type FrameworkContext<'a> = poise::FrameworkContext<'a, Data, CommandError>; + +use crate::messaging::message::CrackedMessage; +use crate::serenity::prelude::SerenityError; -/// Checks if we're in a prefix context or not. -pub fn is_prefix(ctx: Context) -> bool { - matches!(ctx, Context::Prefix(_)) +impl From for SerenityError { + fn from(_e: CrackedError) -> Self { + //let bs = Box::new(e.to_string()); + SerenityError::Other("CrackedError") + } } /// Struct for the cammed down kicking configuration. @@ -87,13 +109,13 @@ impl Default for CamKickConfig { impl Display for CamKickConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut result = String::new(); - result.push_str(&format!("timeout: {:?}\n", self.timeout)); - result.push_str(&format!("guild_id: {:?}\n", self.guild_id)); - result.push_str(&format!("chan_id: {:?}\n", self.chan_id)); - result.push_str(&format!("dc_msg: {:?}\n", self.dc_msg)); + result.push_str(&format!("timeout: {:?}\n", self.timeout)); + result.push_str(&format!("guild_id: {:?}\n", self.guild_id)); + result.push_str(&format!("chan_id: {:?}\n", self.chan_id)); + result.push_str(&format!("dc_msg: {:?}\n", self.dc_msg)); result.push_str(&format!("msg_on_deafen: {}\n", self.msg_on_deafen)); - result.push_str(&format!("msg_on_mute: {}\n", self.msg_on_mute)); - result.push_str(&format!("msg_on_dc: {}\n", self.msg_on_dc)); + result.push_str(&format!("msg_on_mute: {}\n", self.msg_on_mute)); + result.push_str(&format!("msg_on_dc: {}\n", self.msg_on_dc)); write!(f, "{}", result) } @@ -125,12 +147,14 @@ impl Default for BotCredentials { #[derive(Serialize, Deserialize, Clone, Debug)] pub struct BotConfig { pub video_status_poll_interval: Option, + // TODO: Get rid of this, it's redundent with the owners in the serenity library. pub owners: Option>, // Cammed down kicking config pub cam_kick: Option>, pub sys_log_channel_id: Option, pub self_deafen: Option, pub volume: Option, + #[serde(skip)] pub guild_settings_map: Option>, pub prefix: Option, pub credentials: Option, @@ -182,7 +206,7 @@ impl Display for BotConfig { .cloned() .unwrap_or(DEFAULT_PREFIX.to_string()) )); - result.push_str(&format!("credentials: {:?}\n", self.credentials)); + result.push_str(&format!("credentials: {:?}\n", self.credentials.is_some())); result.push_str(&format!("database_url: {:?}\n", self.database_url)); result.push_str(&format!("log_prefix: {:?}\n", self.log_prefix)); write!(f, "{}", result) @@ -288,17 +312,20 @@ impl PhoneCodeData { pub struct DataInner { pub up_prefix: &'static str, pub bot_settings: BotConfig, + pub start_time: SystemTime, // TODO?: Make this a HashMap, pointing to a settings struct containing // user priviledges, etc pub authorized_users: HashSet, + #[serde(skip)] + pub join_vc_tokens: dashmap::DashMap>>, // // Non-serializable below here. What did I even decide to make this Serializable for? // I doubt it's doing anything, most fields aren't. // #[serde(skip)] pub phone_data: PhoneCodeData, - #[serde(skip)] - pub event_log: EventLog, + // #[serde(skip)] + // pub event_log: EventLog, #[serde(skip)] pub event_log_async: EventLogAsync, #[serde(skip)] @@ -307,18 +334,14 @@ pub struct DataInner { pub database_pool: Option, #[serde(skip)] pub http_client: reqwest::Client, - // Synchronous settings and caches. These are going away. - pub guild_settings_map_non_async: - Arc>>, - #[serde(skip)] - pub guild_msg_cache_ordered: Arc>>, - // Async access fields, will switch entirely to these #[serde(skip)] pub guild_settings_map: Arc>>, #[serde(skip)] pub guild_cache_map: Arc>>, #[serde(skip)] + pub guild_msg_cache_ordered: Arc>>, + #[serde(skip)] #[cfg(feature = "crack-gpt")] pub gpt_ctx: Arc>>, } @@ -344,7 +367,7 @@ impl std::fmt::Debug for DataInner { self.guild_msg_cache_ordered )); result.push_str(&format!("guild_cache_map: {:?}\n", self.guild_cache_map)); - result.push_str(&format!("event_log: {:?}\n", self.event_log)); + result.push_str(&format!("event_log: {:?}\n", self.event_log_async)); result.push_str(&format!("database_pool: {:?}\n", self.database_pool)); #[cfg(feature = "crack-gpt")] result.push_str(&format!("gpt_context: {:?}\n", self.gpt_ctx)); @@ -397,24 +420,6 @@ impl DataInner { } } -/// General log for events that the bot reveices from Discord. -#[derive(Clone, Debug)] -pub struct EventLog(pub Arc>); - -impl std::ops::Deref for EventLog { - type Target = Arc>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl std::ops::DerefMut for EventLog { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - /// General log for events that the bot reveices from Discord. #[derive(Clone, Debug)] pub struct EventLogAsync(pub ArcTMutex); @@ -451,24 +456,6 @@ impl Default for EventLogAsync { } } -impl Default for EventLog { - fn default() -> Self { - let log_path = format!("{}/events.log", get_log_prefix()); - let _ = fs::create_dir_all(Path::new(&log_path).parent().unwrap()); - let log_file = match File::create(log_path) { - Ok(f) => f, - Err(e) => { - eprintln!("Error creating log file: {}", e); - // FIXME: Maybe use io::null()? - // I went down this path with sink and it was a mistake. - File::create("/dev/null") - .expect("Should be able to have a file object to write too.") - }, - }; - Self(Arc::new(SyncMutex::new(log_file))) - } -} - impl EventLogAsync { /// Create a new EventLog, calls default pub fn new() -> Self { @@ -497,7 +484,7 @@ impl EventLogAsync { event: obj, }; let mut buf = serde_json::to_vec(&entry).unwrap(); - let _ = buf.write(&[b'\n']); + let _ = buf.write(b"\n"); let buf: &[u8] = buf.as_slice(); self.lock() .await @@ -508,7 +495,7 @@ impl EventLogAsync { /// Write an object to the log file. pub async fn write_obj(&self, obj: &T) -> Result<(), Error> { let mut buf = serde_json::to_vec(obj).unwrap(); - let _ = buf.write(&[b'\n']); + let _ = buf.write(b"\n"); let buf: &[u8] = buf.as_slice(); self.lock() .await @@ -525,86 +512,24 @@ impl EventLogAsync { } } -/// impl of EventLog -impl EventLog { - /// Create a new EventLog, calls default - pub fn new() -> Self { - Self::default() - } - - /// Write an object to the log file without a note. - pub fn write_log_obj(&self, name: &str, obj: &T) -> Result<(), Error> { - self.write_log_obj_note(name, None, obj) - } - - /// Write an object to the log file with a note. - pub fn write_log_obj_note( - &self, - name: &str, - notes: Option<&str>, - obj: &T, - ) -> Result<(), Error> { - let entry = LogEntry { - name: name.to_string(), - notes: notes.unwrap_or("").to_string(), - event: obj, - }; - let mut buf = serde_json::to_vec(&entry).unwrap(); - let _ = buf.write(&[b'\n']); - let buf: &[u8] = buf.as_slice(); - self.lock() - .unwrap() - .write_all(buf) - .map_err(|e| CrackedError::IO(e).into()) - } - - /// Write an object to the log file. - pub fn write_obj(&self, obj: &T) -> Result<(), Error> { - let mut buf = serde_json::to_vec(obj).unwrap(); - let _ = buf.write(&[b'\n']); - let buf: &[u8] = buf.as_slice(); - self.lock() - .unwrap() - .write_all(buf) - .map_err(|e| CrackedError::IO(e).into()) - } - - /// Write a buffer to the log file. - pub fn write(self, buf: &[u8]) -> Result<(), Error> { - self.lock() - .unwrap() - .write_all(buf) - .map_err(|e| CrackedError::IO(e).into()) - } -} - impl Default for DataInner { fn default() -> Self { - // let topgg_token = std::env::var("TOPGG_TOKEN").unwrap_or_default(); - // let runtime = tokio::runtime::Builder::new_multi_thread() - // .worker_threads(4) - // .enable_all() - // .build() - // .unwrap(); - // let rt_handle = Arc::new(RwLock::new(Some(runtime.handle().clone()))); Self { - // rt_handle, phone_data: PhoneCodeData::default(), //PhoneCodeData::load().unwrap(), up_prefix: "R", bot_settings: Default::default(), + start_time: SystemTime::now(), + join_vc_tokens: Default::default(), authorized_users: Default::default(), guild_settings_map: Arc::new(RwLock::new(HashMap::new())), guild_cache_map: Arc::new(Mutex::new(HashMap::new())), - guild_settings_map_non_async: Arc::new(SyncRwLock::new(HashMap::new())), - guild_msg_cache_ordered: Arc::new(SyncMutex::new(BTreeMap::new())), - event_log: EventLog::default(), + guild_msg_cache_ordered: Arc::new(Mutex::new(BTreeMap::new())), event_log_async: EventLogAsync::default(), database_pool: None, http_client: http_utils::get_client().clone(), db_channel: None, #[cfg(feature = "crack-gpt")] gpt_ctx: Arc::new(RwLock::new(None)), - // topgg_client: topgg::Client::new(topgg_token), } } } @@ -626,6 +551,43 @@ impl std::ops::Deref for Data { } } +pub enum MessageOrReplyHandle<'a> { + Message(Message), + ReplyHandle(poise::ReplyHandle<'a>), +} + +impl MessageOrReplyHandle<'_> { + pub async fn into_message(self) -> Option { + match self { + MessageOrReplyHandle::Message(msg) => Some(msg), + MessageOrReplyHandle::ReplyHandle(handle) => handle.into_message().await.ok(), + } + } + + pub async fn delete(self, ctx: Context<'_>) { + match self { + MessageOrReplyHandle::Message(msg) => { + let _ = msg.delete(&ctx).await; + }, + MessageOrReplyHandle::ReplyHandle(handle) => { + let _ = handle.delete(ctx).await; + }, + } + } +} + +impl From for MessageOrReplyHandle<'_> { + fn from(msg: Message) -> Self { + MessageOrReplyHandle::Message(msg) + } +} + +impl<'a: 'b, 'b> From> for MessageOrReplyHandle<'b> { + fn from(handle: poise::ReplyHandle<'a>) -> Self { + MessageOrReplyHandle::ReplyHandle(handle) + } +} + impl Data { /// Insert a guild into the guild settings map. pub async fn insert_guild( @@ -646,31 +608,29 @@ impl Data { guild_id: GuildId, _track: &str, ) -> Result { - let play_log_id = PlayLog::get_last_played_by_guild_metadata( - self.database_pool.as_ref().unwrap(), - guild_id.into(), - ) - .await?; + let pool = self.get_db_pool()?; + let play_log_id = + PlayLog::get_last_played_by_guild_metadata(&pool, guild_id.into(), 1).await?; let pool = self.database_pool.as_ref().unwrap(); let id = *play_log_id.first().unwrap() as i32; - let _ = TrackReaction::insert(pool, id).await; + let _ = TrackReaction::insert(pool, id).await?; TrackReaction::add_dislike(pool, id).await } /// Add a message to the cache - pub fn add_msg_to_cache(&self, guild_id: GuildId, msg: Message) -> Option { + pub async fn add_msg_to_cache(&self, guild_id: GuildId, msg: Message) -> Option { let now = chrono::Utc::now(); - self.add_msg_to_cache_ts(guild_id, now, msg) + self.add_msg_to_cache_ts(guild_id, now, msg).await } /// Add msg to the cache with a timestamp. - pub fn add_msg_to_cache_ts( + pub async fn add_msg_to_cache_ts( &self, guild_id: GuildId, ts: DateTime, msg: Message, ) -> Option { - let mut guild_msg_cache_ordered = self.guild_msg_cache_ordered.lock().unwrap(); + let mut guild_msg_cache_ordered = self.guild_msg_cache_ordered.lock().await; guild_msg_cache_ordered .entry(guild_id) .or_default() @@ -684,7 +644,7 @@ impl Data { guild_id: GuildId, ts: DateTime, ) -> Option { - let mut guild_msg_cache_ordered = self.guild_msg_cache_ordered.lock().unwrap(); + let mut guild_msg_cache_ordered = self.guild_msg_cache_ordered.lock().await; guild_msg_cache_ordered .get_mut(&guild_id) .unwrap() @@ -704,75 +664,40 @@ impl Data { pub fn with_guild_settings_map(&self, guild_settings: GuildSettingsMapParam) -> Self { Self(Arc::new(self.0.with_guild_settings_map(guild_settings))) } -} -/// Trait to extend the Context struct with additional convenience functionality. -pub trait ContextExt { - /// Send a message to tell the worker pool to do a db write when it feels like it. - fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData); - /// Return the call that the bot is currently in, if it is in one. - fn get_call(&self) -> impl Future>, CrackedError>>; - /// Add a message to the cache - fn add_msg_to_cache_nonasync(&self, guild_id: GuildId, msg: Message) -> Option; - /// Gets the channel id that the bot is currently playing in for a given guild. - fn get_active_channel_id(&self, guild_id: GuildId) -> impl Future>; -} - -/// Implement the ContextExt trait for the Context struct. -impl ContextExt for Context<'_> { - /// Send a message to tell the worker pool to do a db write when it feels like it. - fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData) { - let username = ready_track.username.clone(); - let MyAuxMetadata::Data(aux_metadata) = ready_track.metadata.clone(); - let user_id = ready_track.user_id; - let guild_id = self.guild_id().unwrap(); - let channel_id = self.channel_id(); - match &self.data().db_channel { - Some(channel) => { - let write_data: MetadataMsg = MetadataMsg { - user_id, - aux_metadata, - username, - guild_id, - channel_id, - }; - if let Err(e) = channel.try_send(write_data) { - tracing::error!("Error sending metadata to db_channel: {}", e); - } - }, - None => {}, - } + /// Get the database pool for the postgresql database. + pub fn get_db_pool(&self) -> Result { + self.database_pool + .as_ref() + .ok_or(CrackedError::NoDatabasePool) + .cloned() } - /// Return the call that the bot is currently in, if it is in one. - async fn get_call(&self) -> Result>, CrackedError> { - let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; - let manager = songbird::get(self.serenity_context()) + /// Deny a user permission to use the music commands. + pub async fn add_denied_music_user( + &self, + guild_id: GuildId, + user: UserId, + ) -> CrackedResult { + self.guild_settings_map + .write() .await - .ok_or(CrackedError::NotConnected)?; - manager.get(guild_id).ok_or(CrackedError::NotConnected) - } - - /// Add a message to the cache - fn add_msg_to_cache_nonasync(&self, guild_id: GuildId, msg: Message) -> Option { - self.data().add_msg_to_cache(guild_id, msg) - } - - /// Gets the channel id that the bot is currently playing in for a given guild. - async fn get_active_channel_id(&self, guild_id: GuildId) -> Option { - let serenity_context = self.serenity_context(); - let manager = songbird::get(serenity_context) + .entry(guild_id) + .or_insert_with(GuildSettings::default) + .add_denied_music_user(user) .await - .expect("Failed to get songbird manager") - .clone(); - - let call_lock = manager.get(guild_id)?; - let call = call_lock.lock().await; - - let channel_id = call.current_channel()?; - let serenity_channel_id = ChannelId::new(channel_id.0.into()); + } - Some(serenity_channel_id) + /// Check if a user is allowed to use the music commands. + pub async fn check_music_permissions(&self, guild_id: GuildId, user: UserId) -> bool { + if let Some(settings) = self.guild_settings_map.read().await.get(&guild_id).cloned() { + settings + .get_music_permissions() + .map(|x| x.is_user_allowed(user.get())) + .unwrap_or(true) + } else { + true + } } } @@ -797,10 +722,10 @@ mod lib_test { } /// Test the creation of a default EventLog - #[test] - fn test_event_log_default() { - let event_log = EventLog::default(); - let file = event_log.lock().unwrap(); + #[tokio::test] + async fn test_event_log_default() { + let event_log = EventLogAsync::default(); + let file = event_log.lock().await; assert_eq!(file.metadata().unwrap().len(), 0); } @@ -808,8 +733,14 @@ mod lib_test { #[test] fn test_display_cam_kick_config() { let cam_kick = CamKickConfig::default(); - // let want = "timeout: 0\nguild_id: 0\nchan_id: 0\ndc_msg: \"You have been violated for being cammed down for too long.\"\nmsg_on_deafen: false\nmsg_on_mute: false\nmsg_on_dc: false\n"; - let want = "timeout: 0\nguild_id: 0\nchan_id: 0\ndc_msg: \"You have been violated for being cammed down for too long.\"\nmsg_on_deafen: false\nmsg_on_mute: false\nmsg_on_dc: false\n"; + let want = r#"timeout: 0 +guild_id: 0 +chan_id: 0 +dc_msg: "You have been violated for being cammed down for too long." +msg_on_deafen: false +msg_on_mute: false +msg_on_dc: false +"#; assert_eq!(cam_kick.to_string(), want); } diff --git a/crack-core/src/macros.rs b/crack-core/src/macros.rs new file mode 100644 index 000000000..2778fb8d4 --- /dev/null +++ b/crack-core/src/macros.rs @@ -0,0 +1,73 @@ +// Discord TTS Bot +// Copyright (C) 2021-Present David Thomas +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#[macro_export] +macro_rules! require { + ($to_check:expr) => { + require!($to_check, ()) + }; + ($to_check:expr, $ret:expr) => { + if let Some(to_check) = $to_check { + to_check + } else { + return $ret; + } + }; +} + +#[macro_export] +macro_rules! require_guild { + ($ctx:expr) => { + require_guild!($ctx, Ok(())) + }; + ($ctx:expr, $ret:expr) => { + $crate::require!($ctx.guild(), { + ::tracing::warn!( + "Guild {} not cached in {} command!", + $ctx.guild_id().unwrap(), + $ctx.command().qualified_name + ); + $ret + }) + }; +} + +#[macro_export] +macro_rules! bool_enum { + ($name:ident($true_value:ident | $false_value:ident)) => { + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] + pub enum $name { + $true_value, + $false_value, + } + + impl From<$name> for bool { + fn from(value: $name) -> bool { + value == $name::$true_value + } + } + + impl From for $name { + fn from(value: bool) -> Self { + if value { + Self::$true_value + } else { + Self::$false_value + } + } + } + }; +} diff --git a/crack-core/src/messaging/interface.rs b/crack-core/src/messaging/interface.rs index a0ccd5a64..4f51f2b7e 100644 --- a/crack-core/src/messaging/interface.rs +++ b/crack-core/src/messaging/interface.rs @@ -1,31 +1,118 @@ -use super::messages::REQUESTED_BY; +use crate::commands::MyAuxMetadata; use crate::errors::CrackedError; +use crate::http_utils::SendMessageParams; use crate::messaging::messages::{ - QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_NO_SRC, QUEUE_NO_TITLE, - QUEUE_PAGE, QUEUE_PAGE_OF, QUEUE_UP_NEXT, + PROGRESS, QUEUE_NOTHING_IS_PLAYING, QUEUE_NOW_PLAYING, QUEUE_NO_SONGS, QUEUE_NO_SRC, + QUEUE_NO_TITLE, QUEUE_PAGE, QUEUE_PAGE_OF, QUEUE_UP_NEXT, REQUESTED_BY, }; use crate::utils::EMBED_PAGE_SIZE; use crate::utils::{calculate_num_pages, send_embed_response_poise}; -use crate::Context as CrackContext; +use crate::CrackedResult; use crate::{guild::settings::DEFAULT_LYRICS_PAGE_SIZE, utils::create_paged_embed}; use crate::{ messaging::message::CrackedMessage, utils::{ - get_footer_info, get_human_readable_timestamp, get_requesting_user, get_track_metadata, + build_footer_info, get_human_readable_timestamp, get_requesting_user, get_track_metadata, }, + Context as CrackContext, Error, }; /// Contains functions for creating embeds and other messages which are used /// to communicate with the user. use lyric_finder::LyricResult; -use poise::CreateReply; -use serenity::all::UserId; +use poise::{CreateReply, ReplyHandle}; +use serenity::all::EmbedField; +use serenity::all::GuildId; use serenity::{ - all::Mentionable, - all::{ButtonStyle, CreateEmbed}, + all::{ButtonStyle, CreateEmbed, CreateMessage, Message}, + all::{CacheHttp, ChannelId, Mentionable, UserId}, builder::{CreateActionRow, CreateButton, CreateEmbedAuthor, CreateEmbedFooter}, }; +use songbird::input::AuxMetadata; use songbird::tracks::TrackHandle; use std::fmt::Write; +use std::time::Duration; + +//###########################################################################// +// Methods to create embeds for specific messages from services or common +// commands. +//###########################################################################// +// + +// ------ Logging output ------ // + +/// Create and sends an log message as an embed. +/// FIXME: The avatar_url won't always be available. How do we best handle this? +pub async fn build_log_embed( + title: &str, + description: &str, + avatar_url: &str, +) -> Result { + let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let footer = CreateEmbedFooter::new(now_time_str); + Ok(CreateEmbed::default() + .title(title) + .description(description) + .thumbnail(avatar_url) + .footer(footer)) +} + +/// Build a log embed with(out?) a thumbnail. +pub async fn build_log_embed_thumb( + guild_name: &str, + title: &str, + id: &str, + description: &str, + avatar_url: &str, +) -> Result { + let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let footer_str = format!("{} | {} | {}", guild_name, id, now_time_str); + let footer = CreateEmbedFooter::new(footer_str); + let author = CreateEmbedAuthor::new(title).icon_url(avatar_url); + Ok(CreateEmbed::default() + .author(author) + // .title(title) + .description(description) + // .thumbnail(avatar_url) + .footer(footer)) +} + +/// Send a log message as a embed with a thumbnail. +#[cfg(not(tarpaulin_include))] +pub async fn send_log_embed_thumb( + guild_name: &str, + channel: &ChannelId, + cache_http: &impl CacheHttp, + id: &str, + title: &str, + description: &str, + avatar_url: &str, +) -> Result { + let embed = build_log_embed_thumb(guild_name, title, id, description, avatar_url).await?; + + channel + .send_message(cache_http, CreateMessage::new().embed(embed)) + .await + .map_err(Into::into) +} + +/// Create and sends an log message as an embed. +#[cfg(not(tarpaulin_include))] +pub async fn send_log_embed( + channel: &ChannelId, + http: &impl CacheHttp, + title: &str, + description: &str, + avatar_url: &str, +) -> Result { + let embed = build_log_embed(title, description, avatar_url).await?; + + channel + .send_message(http, CreateMessage::new().embed(embed)) + .await + .map_err(Into::into) +} + +// ------ Queue Display / Interaction ------ // /// Converts a user id to a string, with special handling for autoplay. pub fn requesting_user_to_string(user_id: UserId) -> String { @@ -116,39 +203,50 @@ pub async fn create_queue_embed(tracks: &[TrackHandle], page: usize) -> CreateEm ))) } -/// Creates a now playing embed for the given track. -pub async fn create_now_playing_embed(track: &TrackHandle) -> CreateEmbed { - let metadata = get_track_metadata(track).await; +// ------ NOW PLAYING ------ // +// This is probably the message that the use sees // +// the most from the bot. // + +/// Creates an embed from a CrackedMessage and sends it as an embed. +pub fn create_now_playing_embed_metadata( + requesting_user: Option, + cur_position: Option, + metadata: MyAuxMetadata, +) -> CreateEmbed { + let MyAuxMetadata::Data(metadata) = metadata; + tracing::warn!("metadata: {:?}", metadata); + let title = metadata.title.clone().unwrap_or_default(); + let source_url = metadata.source_url.clone().unwrap_or_default(); - let requesting_user = get_requesting_user(track).await; - let position = get_human_readable_timestamp(Some(track.get_info().await.unwrap().position)); + let position = get_human_readable_timestamp(cur_position); let duration = get_human_readable_timestamp(metadata.duration); - let progress_field = ("Progress", format!(">>> {} / {}", position, duration), true); + let progress_field = (PROGRESS, format!(">>> {} / {}", position, duration), true); let channel_field: (&'static str, String, bool) = match requesting_user { - Ok(user_id) => ( + Some(user_id) => ( REQUESTED_BY, format!(">>> {}", requesting_user_to_string(user_id)), true, ), - Err(error) => { - tracing::error!("error getting requesting user: {:?}", error); + None => { + tracing::warn!("No user id"); (REQUESTED_BY, ">>> N/A".to_string(), true) }, }; - let thumbnail = metadata.thumbnail.clone().unwrap_or_default(); - let (footer_text, footer_icon_url, vanity) = get_footer_info(&source_url); + let (footer_text, footer_icon_url, vanity) = build_footer_info(&source_url); + CreateEmbed::new() .author(CreateEmbedAuthor::new(CrackedMessage::NowPlaying)) .title(title.clone()) .url(source_url) .field(progress_field.0, progress_field.1, progress_field.2) .field(channel_field.0, channel_field.1, channel_field.2) + // .thumbnail(url::Url::parse(&thumbnail).unwrap()) .thumbnail( url::Url::parse(&thumbnail) .map(|x| x.to_string()) @@ -162,6 +260,23 @@ pub async fn create_now_playing_embed(track: &TrackHandle) -> CreateEmbed { .footer(CreateEmbedFooter::new(footer_text).icon_url(footer_icon_url)) } +pub async fn track_handle_to_metadata( + track: &TrackHandle, +) -> Result<(Option, Option, MyAuxMetadata), CrackedError> { + let metadata = get_track_metadata(track).await; + let requesting_user = get_requesting_user(track).await.ok(); + let duration = Some(track.get_info().await.unwrap().position); + Ok((requesting_user, duration, MyAuxMetadata::Data(metadata))) +} + +/// Creates a now playing embed for the given track. +pub async fn create_now_playing_embed(track: &TrackHandle) -> CreateEmbed { + let (requesting_user, duration, metadata) = track_handle_to_metadata(track).await.unwrap(); + create_now_playing_embed_metadata(requesting_user, duration, metadata) +} + +// ---------------------- Lyricsd ---------------------------- // + /// Creates a lyrics embed for the given track. pub async fn create_lyrics_embed_old(track: String, artists: String, lyric: String) -> CreateEmbed { CreateEmbed::default() @@ -169,25 +284,14 @@ pub async fn create_lyrics_embed_old(track: String, artists: String, lyric: Stri .title(track) .description(lyric) } - -/// Creates a search results reply. -pub async fn create_search_results_reply(results: Vec) -> CreateReply { - let mut reply = CreateReply::default() - .reply(true) - .content("Search results:"); - for result in results { - reply.embeds.push(result); - } - - reply.clone() -} - /// Creates a paging embed for the lyrics of a song. #[cfg(not(tarpaulin_include))] pub async fn create_lyrics_embed( ctx: CrackContext<'_>, lyric_res: LyricResult, ) -> Result<(), CrackedError> { + use super::messages::UNKNOWN; + let (track, artists, lyric) = match lyric_res { LyricResult::Some { track, @@ -195,8 +299,8 @@ pub async fn create_lyrics_embed( lyric, } => (track, artists, lyric), LyricResult::None => ( - "Unknown".to_string(), - "Unknown".to_string(), + UNKNOWN.to_string(), + UNKNOWN.to_string(), "No lyrics found!".to_string(), ), }; @@ -211,6 +315,8 @@ pub async fn create_lyrics_embed( .await } +// ---------------------- Navigation Buttons ---------------------------- // + /// Builds a single navigation button for the queue. pub fn create_single_nav_btn(label: &str, is_disabled: bool) -> CreateButton { CreateButton::new(label.to_string().to_ascii_lowercase()) @@ -231,8 +337,21 @@ pub fn create_nav_btns(page: usize, num_pages: usize) -> Vec { ])] } +// -------- Search Results -------- // + +/// Creates a search results reply. +pub async fn create_search_results_reply(results: Vec) -> CreateReply { + let mut reply = CreateReply::default() + .reply(true) + .content("Search results:"); + for result in results { + reply.embeds.push(result); + } + + reply.clone() +} /// Sends a message to the user indicating that the search failed. -pub async fn send_search_failed(ctx: CrackContext<'_>) -> Result<(), CrackedError> { +pub async fn send_search_failed<'ctx>(ctx: &'ctx CrackContext<'_>) -> Result<(), CrackedError> { let guild_id = ctx.guild_id().unwrap(); let embed = CreateEmbed::default() .description(format!( @@ -241,21 +360,88 @@ pub async fn send_search_failed(ctx: CrackContext<'_>) -> Result<(), CrackedErro )) .footer(CreateEmbedFooter::new("Search failed!")); let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + ctx.data().add_msg_to_cache(guild_id, msg).await; Ok(()) } /// Sends a message to the user indicating that no query was provided. -pub async fn send_no_query_provided(ctx: CrackContext<'_>) -> Result<(), CrackedError> { - let guild_id = ctx.guild_id().ok_or(CrackedError::NoGuildId)?; +pub async fn send_no_query_provided<'ctx>(ctx: &'ctx CrackContext<'_>) -> Result<(), CrackedError> { let embed = CreateEmbed::default() .description(format!("{}", CrackedError::Other("No query provided!"))) .footer(CreateEmbedFooter::new("No query provided!")); - let msg = send_embed_response_poise(ctx, embed).await?; - ctx.data().add_msg_to_cache(guild_id, msg); + send_embed_response_poise(ctx, embed).await?; Ok(()) } +/// Sends the searching message after a play command is sent. +#[cfg(not(tarpaulin_include))] +pub async fn send_search_message<'ctx>(ctx: &'ctx CrackContext<'_>) -> CrackedResult { + let embed = CreateEmbed::default().description(format!("{}", CrackedMessage::Search)); + let msg = send_embed_response_poise(ctx, embed).await?; + Ok(msg) +} + +/// Send the search results to the user. +pub async fn create_search_response<'ctx>( + ctx: &'ctx CrackContext<'_>, + guild_id: GuildId, + user_id: UserId, + query: String, + res: Vec, +) -> Result { + let author = ctx + .author_member() + .await + .ok_or(CrackedError::AuthorNotFound)?; + let name = author.mention().to_string(); + + let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let fields = build_embed_fields(res).await; + let author = CreateEmbedAuthor::new(name); + let title = format!("Search results for: {}", query); + let footer = CreateEmbedFooter::new(format!("{} * {} * {}", user_id, guild_id, now_time_str)); + let embed = CreateEmbed::new() + .author(author) + .title(title) + .footer(footer) + .fields(fields.into_iter().map(|f| (f.name, f.value, f.inline))); + + send_embed_response_poise(ctx, embed).await +} + +// ---------------------- Joining Channel ---------------------------- // + +use crate::poise_ext::PoiseContextExt; +/// Sends a message to the user indicating that the search failed. +pub async fn send_joining_channel<'ctx>( + ctx: &'ctx CrackContext<'_>, + channel_id: ChannelId, +) -> Result, Error> { + let msg = CrackedMessage::Summon { + mention: channel_id.mention(), + }; + let params = SendMessageParams::new(msg).with_channel(channel_id); + + ctx.send_message(params).await.map_err(Into::into) +} + +// ---------------------- Most Generic Message Function ---------------// + +async fn build_embed_fields(elems: Vec) -> Vec { + use crate::utils::duration_to_string; + tracing::warn!("num elems: {:?}", elems.len()); + let mut fields = vec![]; + // let tmp = "".to_string(); + for elem in elems.into_iter() { + let title = elem.title.unwrap_or_default(); + let link = elem.source_url.unwrap_or_default(); + let duration = elem.duration.unwrap_or_default(); + let elem = format!("({}) - {}", link, duration_to_string(duration)); + fields.push(EmbedField::new(format!("[{}]", title), elem, true)); + } + fields +} + #[cfg(test)] mod test { #[test] diff --git a/crack-core/src/messaging/message.rs b/crack-core/src/messaging/message.rs index 99347a133..f0dfc93fd 100644 --- a/crack-core/src/messaging/message.rs +++ b/crack-core/src/messaging/message.rs @@ -1,22 +1,26 @@ -use std::fmt::Display; +use std::{borrow::Cow, fmt::Display}; -use self::serenity::model::mention::Mention; use ::serenity::builder::CreateEmbed; #[cfg(feature = "crack-osint")] use crack_osint::virustotal::VirusTotalApiResponse; -use poise::serenity_prelude::{self as serenity, UserId}; +use poise::serenity_prelude as serenity; +use serenity::{Mention, Mentionable, UserId}; +use std::time::Duration; -use crate::{errors::CrackedError, messaging::messages::*}; +use crate::{errors::CrackedError, messaging::messages::*, utils::duration_to_string}; const RELEASES_LINK: &str = "https://github.com/cycle-five/cracktunes/releases"; const REPO_LINK: &str = "https://github.com/cycle-five/cracktunes/"; +#[repr(u8)] #[derive(Debug)] pub enum CrackedMessage { AutopauseOff, AutopauseOn, AutoplayOff, AutoplayOn, + AutoRole(serenity::RoleId), + BugNone(String), CategoryCreated { channel_id: serenity::ChannelId, channel_name: String, @@ -34,17 +38,24 @@ pub enum CrackedMessage { Clear, Clean(i32), CrackedError(CrackedError), + CrackedRed(String), + CreateEmbed(Box), + CommandFound(String), DomainInfo(String), Error, ErrorHttp(serenity::http::HttpError), + GrabbedNotice, InvalidIP(String), + InviteLink, IPDetails(String), IPVersion(String), Leaving, LoopDisable, LoopEnable, + NoAutoRole, NowPlaying, Other(String), + OwnersOnly, PaginationComplete, Pause, PasswordPwned, @@ -60,6 +71,7 @@ pub enum CrackedMessage { PlaylistQueued, PlaylistQueuing(String), PlayLog(Vec), + Pong, Premium(bool), PremiumPlug, RemoveMultiple, @@ -70,7 +82,7 @@ pub enum CrackedMessage { }, RoleDeleted { role_id: serenity::RoleId, - role_name: String, + role_name: Cow<'static, String>, }, RoleNotFound, #[cfg(feature = "crack-osint")] @@ -93,6 +105,10 @@ pub enum CrackedMessage { url: String, }, Stop, + SubcommandNotFound { + group: String, + subcommand: String, + }, SocialMediaResponse { response: String, }, @@ -107,6 +123,10 @@ pub enum CrackedMessage { channel_id: serenity::ChannelId, channel_name: String, }, + Uptime { + mention: String, + seconds: u64, + }, UserAuthorized { id: UserId, mention: Mention, @@ -173,9 +193,26 @@ pub enum CrackedMessage { VoiceChannelCreated { channel_name: String, }, + Volume { + vol: f32, + old_vol: f32, + }, WaybackSnapshot { url: String, }, + WelcomeSettings(String), +} + +impl CrackedMessage { + fn discriminant(&self) -> u8 { + unsafe { *(self as *const Self as *const u8) } + } +} + +impl PartialEq for CrackedMessage { + fn eq(&self, other: &Self) -> bool { + self.discriminant() == other.discriminant() + } } impl Display for CrackedMessage { @@ -183,7 +220,13 @@ impl Display for CrackedMessage { match self { Self::AutoplayOff => f.write_str(AUTOPLAY_OFF), Self::AutoplayOn => f.write_str(AUTOPLAY_ON), + Self::AutoRole(role_id) => f.write_str(&format!("{} {}", AUTO_ROLE, role_id.mention())), + Self::BugNone(variable) => f.write_str(&format!("{} {} {}", BUG, variable, BUG_END)), Self::InvalidIP(ip) => f.write_str(&format!("{} {}", ip, FAIL_INVALID_IP)), + Self::InviteLink => f.write_str(&format!( + "{} [{}]({})", + INVITE_TEXT, INVITE_LINK_TEXT, INVITE_URL + )), Self::IPDetails(ip) => f.write_str(&format!("{} **{}**", IP_DETAILS, ip)), Self::IPVersion(ipv) => f.write_str(&format!("**{}**", ipv)), Self::AutopauseOff => f.write_str(AUTOPAUSE_OFF), @@ -202,14 +245,20 @@ impl Display for CrackedMessage { CHANNEL_DELETED, channel_id, channel_name )), Self::CrackedError(err) => f.write_str(&format!("{}", err)), + Self::CrackedRed(s) => f.write_str(s), + Self::CreateEmbed(embed) => f.write_str(&format!("{:#?}", embed)), + Self::CommandFound(s) => f.write_str(s), Self::DomainInfo(info) => f.write_str(info), Self::Error => f.write_str(ERROR), Self::ErrorHttp(err) => f.write_str(&format!("{}", err)), + Self::GrabbedNotice => f.write_str(GRABBED_NOTICE), Self::Leaving => f.write_str(LEAVING), Self::LoopDisable => f.write_str(LOOP_DISABLED), Self::LoopEnable => f.write_str(LOOP_ENABLED), + Self::NoAutoRole => f.write_str(NO_AUTO_ROLE), Self::NowPlaying => f.write_str(QUEUE_NOW_PLAYING), Self::Other(message) => f.write_str(message), + Self::OwnersOnly => f.write_str(OWNERS_ONLY), Self::PaginationComplete => f.write_str(PAGINATION_COMPLETE), Self::PasswordPwned => f.write_str(PASSWORD_PWNED), Self::PasswordSafe => f.write_str(PASSWORD_SAFE), @@ -228,6 +277,7 @@ impl Display for CrackedMessage { f.write_str(&format!("⚠️ **{}** {}", domain, PLAY_FAILED_BLOCKED_DOMAIN)) }, Self::PlayLog(log) => f.write_str(&format!("{}\n{}", PLAY_LOG, log.join("\n"))), + Self::Pong => f.write_str("Pong"), Self::Premium(premium) => f.write_str(&format!("{} {}", PREMIUM, premium)), Self::PremiumPlug => f.write_str(PREMIUM_PLUG), #[cfg(feature = "crack-osint")] @@ -249,6 +299,11 @@ impl Display for CrackedMessage { Self::RoleNotFound => f.write_str(ROLE_NOT_FOUND), Self::Shuffle => f.write_str(SHUFFLED_SUCCESS), Self::Stop => f.write_str(STOPPED), + Self::SubcommandNotFound { group, subcommand } => f.write_str( + &SUBCOMMAND_NOT_FOUND + .replace("{group}", group) + .replace("{subcommand}", subcommand), + ), Self::VoteSkip { mention, missing } => f.write_str(&format!( "{}{} {} {} {}", SKIP_VOTE_EMOJI, mention, SKIP_VOTE_USER, missing, SKIP_VOTE_MISSING @@ -278,6 +333,11 @@ impl Display for CrackedMessage { "{} {} {}", CATEGORY_CREATED, channel_id, channel_name )), + Self::Uptime { mention, seconds } => f.write_str(&format!( + "**{}**\n {}", + mention, + duration_to_string(Duration::from_secs(*seconds)), + )), Self::UserAuthorized { id, mention, @@ -323,7 +383,7 @@ impl Display for CrackedMessage { Self::UserMuted { mention, id } => f.write_str(&format!("{MUTED}\n{mention} {id}")), Self::UserUnmuted { mention, id } => f.write_str(&format!("{UNMUTED}\n{mention} {id}")), Self::Version { current, hash } => f.write_str(&format!( - "{} [{}]({}/tag/v{})\n{}({}/latest)\n{}({}/tree/{})", + "{} [{}]({}/tag/v{})\n{}({}/latest)\n{}({}tree/{})", VERSION, current, RELEASES_LINK, @@ -339,7 +399,11 @@ impl Display for CrackedMessage { }, Self::VoteTopggVoted => f.write_str(VOTE_TOPGG_VOTED), Self::VoteTopggNotVoted => f.write_str(VOTE_TOPGG_NOT_VOTED), + Self::Volume { vol, old_vol } => { + f.write_str(&format!("{}: {}\n{}: {}", VOLUME, vol, OLD_VOLUME, old_vol)) + }, Self::WaybackSnapshot { url } => f.write_str(&format!("{} {}", WAYBACK_SNAPSHOT, url)), + Self::WelcomeSettings(settings) => f.write_str(settings), } } } @@ -355,3 +419,239 @@ impl From for CreateEmbed { CreateEmbed::default().description(message.to_string()) } } + +impl From for CrackedMessage { + fn from(error: CrackedError) -> Self { + Self::CrackedError(error) + } +} + +impl From for CrackedMessage { + fn from(error: serenity::http::HttpError) -> Self { + Self::ErrorHttp(error) + } +} + +impl Default for CrackedMessage { + fn default() -> Self { + Self::Other("(default)".to_string()) + } +} + +use colored::Color; +impl From for Color { + fn from(message: CrackedMessage) -> Color { + match message { + CrackedMessage::Error => Color::Red, + CrackedMessage::ErrorHttp(_) => Color::Red, + CrackedMessage::CrackedError(_) => Color::Red, + CrackedMessage::CrackedRed(_) => Color::Red, + CrackedMessage::Other(_) => Color::Yellow, + _ => Color::Blue, + } + } +} + +impl From<&CrackedMessage> for Color { + fn from(message: &CrackedMessage) -> Color { + match message { + CrackedMessage::Error => Color::Red, + CrackedMessage::ErrorHttp(_) => Color::Red, + CrackedMessage::CrackedError(_) => Color::Red, + CrackedMessage::CrackedRed(_) => Color::Red, + CrackedMessage::Other(_) => Color::Yellow, + _ => Color::Blue, + } + } +} + +use serenity::Colour; +impl From for Colour { + fn from(message: CrackedMessage) -> Colour { + match message { + CrackedMessage::Error => Colour::RED, + CrackedMessage::ErrorHttp(_) => Colour::RED, + CrackedMessage::CrackedError(_) => Colour::RED, + CrackedMessage::CrackedRed(_) => Colour::RED, + CrackedMessage::Other(_) => Colour::GOLD, + _ => Colour::BLUE, + } + } +} + +impl From<&CrackedMessage> for Colour { + fn from(message: &CrackedMessage) -> Colour { + match message { + CrackedMessage::Error => Colour::RED, + CrackedMessage::ErrorHttp(_) => Colour::RED, + CrackedMessage::CrackedError(_) => Colour::RED, + CrackedMessage::CrackedRed(_) => Colour::RED, + CrackedMessage::Other(_) => Colour::GOLD, + _ => Colour::BLUE, + } + } +} + +impl From<&CrackedMessage> for Option { + fn from(message: &CrackedMessage) -> Option { + match message { + CrackedMessage::CreateEmbed(embed) => Some(*embed.clone()), + _ => None, + } + } +} + +impl From for crate::CrackedResult2 { + fn from(msg: CrackedMessage) -> crate::CrackedResult2 { + crate::CrackedResult2::Ok(msg) + } +} + +#[cfg(test)] +mod test { + use super::CrackedMessage; + use poise::serenity_prelude as serenity; + + #[test] + fn test_discriminant() { + let message = CrackedMessage::AutopauseOff; + assert_eq!(message.discriminant(), 0); + + let message = CrackedMessage::AutopauseOn; + assert_eq!(message.discriminant(), 1); + + let message = CrackedMessage::AutoplayOff; + assert_eq!(message.discriminant(), 2); + + let message = CrackedMessage::AutoplayOn; + assert_eq!(message.discriminant(), 3); + + let message = CrackedMessage::Clear; + assert_eq!(message.discriminant(), 10); + } + + #[test] + fn test_eq() { + let message = CrackedMessage::AutopauseOff; + assert_eq!(message, CrackedMessage::AutopauseOff); + + let message = CrackedMessage::AutopauseOn; + assert_eq!(message, CrackedMessage::AutopauseOn); + + let message = CrackedMessage::BugNone("test".to_string()); + assert_eq!(message, CrackedMessage::BugNone("test".to_string())); + + let message = CrackedMessage::InvalidIP("test".to_string()); + assert_eq!(message, CrackedMessage::InvalidIP("test".to_string())); + + let message = CrackedMessage::IPDetails("test".to_string()); + assert_eq!(message, CrackedMessage::IPDetails("test".to_string())); + + let message = CrackedMessage::IPVersion("test".to_string()); + assert_eq!(message, CrackedMessage::IPVersion("test".to_string())); + + let message = CrackedMessage::AutopauseOff; + assert_eq!(message, CrackedMessage::AutopauseOff); + + let message = CrackedMessage::AutopauseOn; + assert_eq!(message, CrackedMessage::AutopauseOn); + + let message = CrackedMessage::CountryName("test".to_string()); + assert_eq!(message, CrackedMessage::CountryName("test".to_string())); + + let message = CrackedMessage::Clear; + assert_eq!(message, CrackedMessage::Clear); + + let message = CrackedMessage::Clean(1); + assert_eq!(message, CrackedMessage::Clean(1)); + + let message = CrackedMessage::ChannelSizeSet { + id: serenity::ChannelId::default(), + name: "test".to_string(), + size: 1, + }; + assert_eq!( + message, + CrackedMessage::ChannelSizeSet { + id: serenity::ChannelId::default(), + name: "test".to_string(), + size: 1 + } + ); + + let message = CrackedMessage::ChannelDeleted { + channel_id: serenity::ChannelId::default(), + channel_name: "test".to_string(), + }; + assert_eq!( + message, + CrackedMessage::ChannelDeleted { + channel_id: serenity::ChannelId::default(), + channel_name: "test".to_string() + } + ); + } + + #[test] + fn test_ne() { + let message = CrackedMessage::AutopauseOff; + assert_ne!(message, CrackedMessage::AutopauseOn); + + let message = CrackedMessage::AutopauseOn; + assert_ne!(message, CrackedMessage::AutopauseOff); + + let message = CrackedMessage::BugNone("test".to_string()); + assert_ne!(message, CrackedMessage::InvalidIP("test".to_string())); + + let message = CrackedMessage::InvalidIP("test".to_string()); + assert_ne!(message, CrackedMessage::BugNone("test".to_string())); + + let message = CrackedMessage::IPDetails("test".to_string()); + assert_ne!(message, CrackedMessage::IPVersion("test".to_string())); + + let message = CrackedMessage::IPVersion("test".to_string()); + assert_ne!(message, CrackedMessage::IPDetails("test".to_string())); + + let message = CrackedMessage::AutopauseOff; + assert_ne!(message, CrackedMessage::AutopauseOn); + + let message = CrackedMessage::AutopauseOn; + assert_ne!(message, CrackedMessage::AutopauseOff); + + let message = CrackedMessage::CountryName("test".to_string()); + assert_ne!(message, CrackedMessage::Clear); + + let message = CrackedMessage::Clear; + assert_ne!(message, CrackedMessage::CountryName("test".to_string())); + + let message = CrackedMessage::Clean(1); + assert_ne!( + message, + CrackedMessage::ChannelSizeSet { + id: serenity::ChannelId::default(), + name: "test".to_string(), + size: 1, + } + ); + + let message = CrackedMessage::ChannelSizeSet { + id: serenity::ChannelId::default(), + name: "test".to_string(), + size: 1, + }; + assert_ne!(message, CrackedMessage::Clean(1)); + + let message = CrackedMessage::ChannelDeleted { + channel_id: serenity::ChannelId::default(), + channel_name: "test".to_string(), + }; + assert_ne!( + message, + CrackedMessage::ChannelSizeSet { + id: serenity::ChannelId::default(), + name: "test".to_string(), + size: 1, + } + ); + } +} diff --git a/crack-core/src/messaging/messages.rs b/crack-core/src/messaging/messages.rs index 0e50f7ea7..f80b173cc 100644 --- a/crack-core/src/messaging/messages.rs +++ b/crack-core/src/messaging/messages.rs @@ -9,8 +9,13 @@ pub const CHANNEL_SIZE_SET: &str = "🗑️ Channel size set!"; pub const CHANNEL_DELETED: &str = "🗑️ Deleted channel!"; pub const AUTHORIZED: &str = "✅ User has been authorized."; -pub const DEAUTHORIZED: &str = "❌ User has been deauthorized."; +pub const AUTO_ROLE: &str = "Auto Role"; pub const BANNED: &str = "Banned"; +pub const BUG: &str = "🐞 Bug!"; +pub const BUG_END: &str = "was None!"; +pub const BUG_REPORTED: &str = "🐞 Bug Reported!"; +pub const BUG_REPORT: &str = "🐞 Bug Report"; +pub const DEAUTHORIZED: &str = "❌ User has been deauthorized."; pub const UNBANNED: &str = "Unbanned"; // Use the unicode emoji for the check mark pub const EMOJI_HEADPHONES: &str = "🎧"; @@ -29,54 +34,60 @@ pub const DOMAIN_FORM_BANNED_PLACEHOLDER: &str = "Add domains separated by \';\'. If left blank, all (except for allowed) are blocked by default."; pub const DOMAIN_FORM_TITLE: &str = "Manage sources"; +pub const EMPTY_SEARCH_RESULT: &str = "⚠️ No search results found!"; pub const ERROR: &str = "Fatality! Something went wrong ☹️"; -pub const FAIL_ALREADY_HERE: &str = "⚠️ I'm already here!"; -pub const FAIL_ANOTHER_CHANNEL: &str = "⚠️ I'm already connected to"; +pub const EXTRA_TEXT_AT_BOTTOM: &str = + "This is a friendly cracking, smoking parrot that plays music."; +pub const FAIL_ALREADY_HERE: &str = "⚠️ I'm already here!"; +pub const FAIL_ANOTHER_CHANNEL: &str = "⚠️ I'm already connected to"; pub const FAIL_AUDIO_STREAM_RUSTY_YTDL_METADATA: &str = - "⚠️ Failed to fetch metadata from rusty_ytdl!"; -pub const FAIL_AUTHOR_DISCONNECTED: &str = "⚠️ You are not connected to"; + "⚠️ Failed to fetch metadata from rusty_ytdl!"; +pub const FAIL_AUTHOR_DISCONNECTED: &str = "⚠️ You are not connected to"; ///? -pub const FAIL_AUTHOR_NOT_FOUND: &str = "⚠️ Could not find you in any voice channel!"; -pub const FAIL_LOOP: &str = "⚠️ Failed to toggle loop!"; -pub const FAIL_EMPTY_VECTOR: &str = "⚠️ Empty vector not allowed!"; -pub const FAIL_INSERT: &str = "⚠️ Failed to insert!"; -pub const FAIL_INVALID_TOPGG_TOKEN: &str = "⚠️ Invalid top.gg token!"; -pub const FAIL_INVALID_PERMS: &str = "⚠️ Invalid permissions!!"; -pub const FAIL_MINUTES_PARSING: &str = "⚠️ Invalid formatting for 'minutes'"; -pub const FAIL_NO_SONG_ON_INDEX: &str = "⚠️ There is no queued song on that index!"; -pub const FAIL_NO_SONGBIRD: &str = "⚠️ Failed to get songbird!"; +pub const FAIL_AUTHOR_NOT_FOUND: &str = "⚠️ Could not find you in any voice channel!"; +pub const FAIL_LOOP: &str = "⚠️ Failed to toggle loop!"; +pub const FAIL_EMPTY_VECTOR: &str = "⚠️ Empty vector not allowed!"; +pub const FAIL_INSERT: &str = "⚠️ Failed to insert!"; +pub const FAIL_INVALID_TOPGG_TOKEN: &str = "⚠️ Invalid top.gg token!"; +pub const FAIL_INVALID_PERMS: &str = "⚠️ Invalid permissions!!"; +pub const FAIL_MINUTES_PARSING: &str = "⚠️ Invalid formatting for 'minutes'"; +pub const FAIL_NO_SONG_ON_INDEX: &str = "⚠️ There is no queued song on that index!"; +pub const FAIL_NO_SONGBIRD: &str = "⚠️ Failed to get songbird!"; pub const FAIL_NO_VIRUSTOTAL_API_KEY: &str = - "⚠️ The VIRUS_TOTAL_API_KEY environment variable is not set!"; -pub const FAIL_NO_VOICE_CONNECTION: &str = "⚠️ I'm not connected to any voice channel!"; + "⚠️ The VIRUS_TOTAL_API_KEY environment variable is not set!"; +pub const FAIL_NO_VOICE_CONNECTION: &str = "⚠️ I'm not connected to any voice channel!"; +pub const FAIL_NO_QUERY_PROVIDED: &str = "⚠️ No query provided!"; pub const FAIL_NOT_IMPLEMENTED: &str = "⚠️ Function is not implemented!"; -pub const FAIL_NOTHING_PLAYING: &str = "🔈 Nothing is playing!"; -pub const FAIL_REMOVE_RANGE: &str = "⚠️ `until` needs to be higher than `index`!"; -pub const FAIL_SECONDS_PARSING: &str = "⚠️ Invalid formatting for 'seconds'"; -pub const FAIL_TO_SET_CHANNEL_SIZE: &str = "⚠️ Failed to set channel size!"; -pub const FAIL_WRONG_CHANNEL: &str = "⚠️ We are not in the same voice channel!"; -pub const FAIL_PARSE_TIME: &str = "⚠️ Failed to parse time, speak English much?"; -pub const FAIL_PLAYLIST_FETCH: &str = "⚠️ Failed to fetch playlist!"; -pub const FAIL_INVALID_IP: &str = "⚠️ Invalid IP address!"; +pub const FAIL_NOTHING_PLAYING: &str = "🔈 Nothing is playing!"; +pub const FAIL_REMOVE_RANGE: &str = "⚠️ `until` needs to be higher than `index`!"; +pub const FAIL_SECONDS_PARSING: &str = "⚠️ Invalid formatting for 'seconds'"; +pub const FAIL_TO_SET_CHANNEL_SIZE: &str = "⚠️ Failed to set channel size!"; +pub const FAIL_WRONG_CHANNEL: &str = "⚠️ We are not in the same voice channel!"; +pub const FAIL_PARSE_TIME: &str = "⚠️ Failed to parse time, speak English much?"; +pub const FAIL_PLAYLIST_FETCH: &str = "⚠️ Failed to fetch playlist!"; +pub const FAIL_INVALID_IP: &str = "⚠️ Invalid IP address!"; -pub const EMPTY_SEARCH_RESULT: &str = "⚠️ No search results found!"; -pub const GUILD_ONLY: &str = "⚠️ This command can only be used in a server!"; -pub const IDLE_ALERT: &str = "⚠️ I've been idle for a while so I'm going to hop off, set the idle timeout to change this! Also support my development and I won't have to premium-gate features!\n[CrackTunes Patreon](https://patreon.com/CrackTunes)"; -pub const PREMIUM_PLUG: &str = "👑 Like the bot? Support my development and keep it premium-free for everyone!\n[CrackTunes Patreon](https://patreon.com/CrackTunes)"; -pub const IP_DETAILS: &str = "🌐 IP details for"; +pub const GUILD_ONLY: &str = "⚠️ This command can only be used in a server!"; +pub const IDLE_ALERT: &str = "⚠️ I've been idle for a while so I'm going to hop off, set the idle timeout to change this! Also support my development and I won't have to premium-gate features!\n[CrackTunes Patreon](https://patreon.com/CrackTunes)"; +pub const IP_DETAILS: &str = "🌐 IP details for"; pub const JOINING: &str = "Joining"; -pub const LEAVING: &str = "👋 See you soon!"; -pub const LOOP_DISABLED: &str = "🔁 Disabled loop!"; -pub const LOOP_ENABLED: &str = "🔁 Enabled loop!"; -pub const NOT_IN_MUSIC_CHANNEL: &str = "⚠️ You are not in the music channel! Use"; -pub const NO_CHANNEL_ID: &str = "⚠️ No ChannelId Found!"; -pub const NO_DATABASE_POOL: &str = "⚠️ No Database Pool Found!"; -pub const NO_GUILD_CACHED: &str = "⚠️ No Cached Guild Found!"; -pub const NO_GUILD_ID: &str = "⚠️ No GuildId Found!"; -pub const NO_GUILD_SETTINGS: &str = "⚠️ No GuildSettings Found!"; +pub const KICKED: &str = "Kicked"; +pub const GRABBED_NOTICE: &str = "📃 Sent you a DM with the current track!"; +pub const LEAVING: &str = "👋 See you soon!"; +pub const LOOP_DISABLED: &str = "🔁 Disabled loop!"; +pub const LOOP_ENABLED: &str = "🔁 Enabled loop!"; +pub const NO_AUTO_ROLE: &str = "⚠️ No auto role set for this server!"; +pub const NO_CHANNEL_ID: &str = "⚠️ No ChannelId Found!"; +pub const NO_DATABASE_POOL: &str = "⚠️ No Database Pool Found!"; +pub const NO_GUILD_CACHED: &str = "⚠️ No Cached Guild Found!"; +pub const NO_GUILD_ID: &str = "⚠️ No GuildId Found!"; +pub const NO_GUILD_SETTINGS: &str = "⚠️ No GuildSettings Found!"; pub const NO_USER_AUTOPLAY: &str = "(auto)"; +pub const NOT_IN_MUSIC_CHANNEL: &str = "⚠️ You are not in the music channel! Use"; pub const ONETWOFT: &str = "https://12ft.io/"; +pub const OWNERS_ONLY: &str = "⚠️ This command can only be used by bot owners!"; pub const PAGINATION_COMPLETE: &str = - "🔚 Dynamic message timed out! Run the command again to see updates."; + "🔚 Dynamic message timed out! Run the command again to see updates."; pub const PASSWORD_PWNED: &str = "⚠️ This password has been pwned!"; pub const PASSWORD_SAFE: &str = "🔒 This password is safe!"; pub const PAUSED: &str = "⏸️ Paused!"; @@ -86,67 +97,73 @@ pub const PLAYLIST_ADD: &str = "📃 Added to playlist!"; pub const PLAYLIST_REMOVE: &str = "❌ Removed from playlist!"; pub const PLAYLIST_LIST_EMPTY: &str = "📃 You have no playlists currently."; pub const PLAYLIST_EMPTY: &str = "📃 This playlist has no songs!"; +pub const PLAYLISTS: &str = "Playlists"; pub const PLAY_FAILED_BLOCKED_DOMAIN: &str = "**is either not allowed in this server or is not supported!** \n\nTo explicitely allow this domain, ask a moderator to run the `/managesources` command. [Click to see a list of supported sources.](https://github.com/yt-dlp/yt-dlp/blob/master/supportedsites.md)"; pub const PLAY_ALL_FAILED: &str = - "⚠️ Cannot fetch playlist via keywords! Try passing this command an URL."; -pub const PLAY_PLAYLIST: &str = "📃 Added playlist to queue!"; -pub const PLAY_SEARCH: &str = "🔎 Searching..."; -pub const PLAY_QUEUE: &str = "📃 Added to queue!"; -pub const PLAY_TOP: &str = "📃 Added to top!"; -pub const PLAY_LOG: &str = "🎵 Last Played Songs"; -pub const PHONE_NUMBER_INFO_ERROR: &str = "⚠️ Failed to fetch phone number info!"; + "⚠️ Cannot fetch playlist via keywords! Try passing this command an URL."; +pub const PLAY_PLAYLIST: &str = "📃 Added playlist to queue!"; +pub const PLAY_SEARCH: &str = "🔎 Searching..."; +pub const PLAY_QUEUE: &str = "📃 Added to queue!"; +pub const PLAY_TOP: &str = "📃 Added to top!"; +pub const PLAY_LOG: &str = "🎵 Last Played Songs"; +pub const PREMIUM: &str = "👑 Premium status:"; +pub const PREMIUM_PLUG: &str = "👑 Like the bot? Support my development and keep it premium-free for everyone!\n[CrackTunes Patreon](https://patreon.com/CrackTunes)"; +pub const PROGRESS: &str = "Progress"; +pub const PHONE_NUMBER_INFO_ERROR: &str = "⚠️ Failed to fetch phone number info!"; pub const QUEUE_EXPIRED: &str = "This command has expired.\nPlease feel free to reinvoke it!"; pub const QUEUE_IS_EMPTY: &str = "Queue is empty!"; pub const QUEUE_NO_SONGS: &str = "There's no songs up next!"; pub const QUEUE_NO_TITLE: &str = "Unknown title"; pub const QUEUE_NO_SRC: &str = "Unknown source url"; pub const QUEUE_NOTHING_IS_PLAYING: &str = "Nothing is playing!"; -pub const QUEUE_NOW_PLAYING: &str = "🔊 Now playing"; +pub const QUEUE_NOW_PLAYING: &str = "🔊 Now playing"; pub const QUEUE_PAGE_OF: &str = "of"; pub const QUEUE_PAGE: &str = "Page"; -pub const QUEUE_UP_NEXT: &str = "⌛ Up next"; -pub const REMOVED_QUEUE_MULTIPLE: &str = "❌ Removed multiple tracks from queue!"; -pub const REMOVED_QUEUE: &str = "❌ Removed from queue"; -pub const RESUMED: &str = "▶️ Resumed!"; +pub const QUEUE_UP_NEXT: &str = "⌛ Up next"; +pub const REMOVED_QUEUE_MULTIPLE: &str = "❌ Removed multiple tracks from queue!"; +pub const REMOVED_QUEUE: &str = "❌ Removed from queue"; +pub const RESUMED: &str = "▶️ Resumed!"; pub const REQUESTED_BY: &str = "Requested by"; -pub const ROLE_CREATED: &str = "📝 Created role!"; -pub const ROLE_DELETED: &str = "🗑️ Deleted role!"; -pub const ROLE_NOT_FOUND: &str = "⚠️ Role not found!"; -pub const PREMIUM: &str = "👑 Premium status now"; -pub const PROGRESS: &str = "Progress"; -pub const SCAN_QUEUED: &str = "🔍 Scan queued! Use"; -pub const SEARCHING: &str = "🔎 Searching..."; -pub const SEEKED: &str = "⏩ Seeked current track to"; -pub const SHUFFLED_SUCCESS: &str = "🔀 Shuffled successfully!"; -pub const SKIP_VOTE_EMOJI: &str = "🗳 "; +pub const ROLE_CREATED: &str = "📝 Created role!"; +pub const ROLE_DELETED: &str = "🗑️ Deleted role!"; +pub const ROLE_NOT_FOUND: &str = "⚠️ Role not found!"; +pub const SCAN_QUEUED: &str = "🔍 Scan queued! Use"; +pub const SEARCHING: &str = "🔎 Searching..."; +pub const SEEKED: &str = "⏩ Seeked current track to"; +pub const SHUFFLED_SUCCESS: &str = "🔀 Shuffled successfully!"; +pub const SKIP_VOTE_EMOJI: &str = "🗳"; pub const SKIP_VOTE_MISSING: &str = "more vote(s) needed to skip!"; pub const SKIP_VOTE_USER: &str = "has voted to skip!"; -pub const SKIPPED_ALL: &str = "⏭️ Skipped until infinity!"; -pub const SKIPPED_TO: &str = "⏭️ Skipped to"; -pub const SKIPPED: &str = "⏭️ Skipped!"; -pub const SPOTIFY_AUTH_FAILED: &str = "⚠️ **Could not authenticate with Spotify!**\nDid you forget to provide your Spotify application's client ID and secret?"; +pub const SKIPPED_ALL: &str = "⏭️ Skipped until infinity!"; +pub const SKIPPED_TO: &str = "⏭️ Skipped to"; +pub const SKIPPED: &str = "⏭️ Skipped!"; +pub const SPOTIFY_AUTH_FAILED: &str = "⚠️ **Could not authenticate with Spotify!**\nDid you forget to provide your Spotify application's client ID and secret?"; pub const SPOTIFY_INVALID_QUERY: &str = - "⚠️ **Could not find any tracks with that link!**\nAre you sure that is a valid Spotify URL?"; -pub const SPOTIFY_PLAYLIST_FAILED: &str = "⚠️ **Failed to fetch playlist!**\nIt's likely that this playlist is either private or a personalized playlist generated by Spotify, like your daylist."; -pub const STOPPED: &str = "⏹️ Stopped!"; -pub const TIMEOUT: &str = "⏱️ User Timed Out!"; + "⚠️ **Could not find any tracks with that link!**\nAre you sure that is a valid Spotify URL?"; +pub const SPOTIFY_PLAYLIST_FAILED: &str = "⚠️ **Failed to fetch playlist!**\nIt's likely that this playlist is either private or a personalized playlist generated by Spotify, like your daylist."; +pub const STOPPED: &str = "⏹️ Stopped!"; +pub const SUGGESTION: &str = "📝 Suggestion"; +pub const SUBCOMMAND_NOT_FOUND: &str = "⚠️ Subcommand {subcommand} for group {group} not found!"; +pub const TIMEOUT: &str = "⏱️ User Timed Out!"; +pub const TRACK_DURATION: &str = "Track duration:"; +pub const TRACK_NOT_FOUND: &str = "⚠️ **Could not play track!**\nYour request yielded no results."; +pub const TRACK_INAPPROPRIATE: &str = "⚠️ **Could not play track!**\nThe video you requested may be inappropriate for some users, so sign-in is required."; +pub const TRACK_TIME_TO_PLAY: &str = "Estimated time until play:"; +pub const TEST: &str = "🔧 Test"; +pub const TEXT_CHANNEL_CREATED: &str = "📝 Created text channel!"; +pub const CATEGORY_CREATED: &str = "📝 Created category!"; pub const UNTIL: &str = "Until"; -pub const TRACK_DURATION: &str = "Track duration: "; -pub const TRACK_NOT_FOUND: &str = "⚠️ **Could not play track!**\nYour request yielded no results."; -pub const TRACK_INAPPROPRIATE: &str = "⚠️ **Could not play track!**\nThe video you requested may be inappropriate for some users, so sign-in is required."; -pub const TRACK_TIME_TO_PLAY: &str = "Estimated time until play: "; -pub const TEXT_CHANNEL_CREATED: &str = "📝 Created text channel!"; -pub const CATEGORY_CREATED: &str = "📝 Created category!"; -pub const UNAUTHORIZED_USER: &str = "⚠️ You are not authorized to use this command!"; -pub const UNKNOWN_LIT: &str = "Unknown"; +pub const UNKNOWN: &str = "Unknown"; +pub const UNAUTHORIZED_USER: &str = "⚠️ You are not authorized to use this command!"; +pub const UNKNOWN_LIT: &str = UNKNOWN; pub const WAYBACK_SNAPSHOT: &str = "Wayback snapshot for"; -pub const KICKED: &str = "Kicked"; pub const VERSION_LATEST: &str = "Find the latest version [here]"; pub const VERSION: &str = "Version"; pub const VERSION_LATEST_HASH: &str = "Build hash [here]"; -pub const VOLUME: &str = "🔊 Volume"; -pub const VOICE_CHANNEL_CREATED: &str = "🔊 Created voice channel!"; +pub const VOLUME: &str = "🔊 Volume"; +pub const OLD_VOLUME: &str = "Old Volume"; +pub const VOICE_CHANNEL_CREATED: &str = "🔊 Created voice channel!"; pub const VOTE_TOPGG_TEXT: &str = "✅ Vote for CrackTunes on"; pub const VOTE_TOPGG_LINK_TEXT: &str = "top.gg!"; diff --git a/crack-core/src/metrics.rs b/crack-core/src/metrics.rs index 8d5c641d9..c4c17a7f8 100644 --- a/crack-core/src/metrics.rs +++ b/crack-core/src/metrics.rs @@ -41,6 +41,24 @@ mod metrics_internal { .expect("collector can be registered"); } + /// Prometheus handler + #[cfg(feature = "crack-metrics")] + #[cfg(not(tarpaulin_include))] + async fn metrics_handler() -> Result { + let encoder = TextEncoder::new(); + let mut metric_families = prometheus::gather(); + metric_families.extend(REGISTRY.gather()); + // tracing::info!("Metrics: {:?}", metric_families); + let mut buffer = vec![]; + encoder.encode(&metric_families, &mut buffer).unwrap(); + + Ok(warp::reply::with_header( + buffer, + "content-type", + encoder.format_type(), + )) + } + #[cfg(test)] mod test { use super::*; diff --git a/crack-core/src/poise_ext.rs b/crack-core/src/poise_ext.rs new file mode 100644 index 000000000..e43331946 --- /dev/null +++ b/crack-core/src/poise_ext.rs @@ -0,0 +1,563 @@ +use crate::commands::play_utils::TrackReadyData; +use crate::commands::{has_voted_bot_id, MyAuxMetadata}; +use crate::db; +use crate::db::{MetadataMsg, PlayLog}; +use crate::guild::operations::GuildSettingsOperations; +use crate::guild::settings::GuildSettings; +use crate::http_utils; +use crate::Error; +use crate::{ + commands::CrackedError, http_utils::SendMessageParams, messaging::message::CrackedMessage, + utils, utils::OptionTryUnwrap, CrackedResult, Data, +}; +use colored::Colorize; +use core::time::Duration; +use poise::serenity_prelude as serenity; +use poise::{CreateReply, ReplyHandle}; +use serenity::all::{ChannelId, CreateEmbed, GuildId, Message, UserId}; +use songbird::input::AuxMetadata; +use songbird::tracks::TrackQueue; +use songbird::Call; +use std::{future::Future, sync::Arc}; +use tokio::sync::Mutex; + +/// TODO: Separate all the messaging related functions from the other extensions and +/// put them into this extension. +#[allow(dead_code)] +pub trait MessageInterfaceCtxExt { + /// Send a message notifying the user they found a command. + fn send_found_command( + &self, + command: String, + ) -> impl Future, Error>>; + + /// Send a message to the user with the invite link for the bot. + fn send_invite_link(&self) -> impl Future, Error>>; + + fn send_reply( + &self, + message: CrackedMessage, + as_embed: bool, + ) -> impl Future, CrackedError>>; + + /// Sends a message ecknowledging that the user has grabbed the current track. + fn send_grabbed_notice(&self) -> impl Future, Error>>; + + /// Send a now playing message + fn send_now_playing( + &self, + chan_id: ChannelId, + cur_pos: Option, + metadata: Option, + ) -> impl Future>; +} + +impl MessageInterfaceCtxExt for crate::Context<'_> { + /// Sends a message notifying the use they found a command. + async fn send_found_command(&self, command: String) -> Result { + utils::send_reply_embed(self, CrackedMessage::CommandFound(command)).await + } + + async fn send_invite_link(&self) -> Result { + utils::send_reply_embed(self, CrackedMessage::InviteLink).await + } + + async fn send_reply( + &self, + message: CrackedMessage, + as_embed: bool, + ) -> Result { + let color = serenity::Colour::from(&message); + let params = SendMessageParams::new(message) + .with_color(color) + .with_as_embed(as_embed); + let handle = self.send_message(params).await?; + //Ok(handle.into_message().await?) + Ok(handle) + } + + async fn send_grabbed_notice(&self) -> Result { + utils::send_reply_embed(self, CrackedMessage::GrabbedNotice).await + } + + async fn send_now_playing( + &self, + chan_id: ChannelId, + cur_pos: Option, + metadata: Option, + ) -> Result { + let call = self.get_call().await?; + // We don't add this message to the cache because we shouldn't delete it. + utils::send_now_playing( + chan_id, + self.serenity_context().http.clone(), + call, + cur_pos, + metadata, + ) + .await + } +} + +/// Trait to extend the Context struct with additional convenience functionality. +pub trait ContextExt { + /// Send a message to tell the worker pool to do a db write when it feels like it. + fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData); + /// The the user id for the author of the message that created this context. + fn get_user_id(&self) -> serenity::UserId; + /// Gets the log of last played songs on the bot by a specific user + fn get_last_played_by_user( + &self, + user_id: UserId, + ) -> impl Future, CrackedError>>; + + fn get_guild_settings(&self, guild_id: GuildId) -> impl Future>; + + /// Gets the log of last played songs on the bot + fn get_last_played(&self) -> impl Future, CrackedError>>; + /// Return the call that the bot is currently in, if it is in one. + fn get_call(&self) -> impl Future>, CrackedError>>; + /// Return the call and the guild id. This is convenience function I found I had many cases for. + fn get_call_guild_id( + &self, + ) -> impl Future>, GuildId), CrackedError>>; + /// Return the queue owned. + fn get_queue(&self) -> impl Future>; + /// Return the db pool for database operations. + fn get_db_pool(&self) -> Result; + /// Add a message to the cache + fn add_msg_to_cache( + &self, + guild_id: GuildId, + msg: Message, + ) -> impl Future>; + /// Gets the channel id that the bot is currently playing in for a given guild. + fn get_active_channel_id(&self, guild_id: GuildId) -> impl Future>; + + // ----- Send message utility functions ------ // + + /// Send a message notifying the user they found a command. + fn send_found_command( + &self, + command: String, + ) -> impl Future, Error>>; + + /// Send a message to the user with the invite link for the bot. + fn send_invite_link(&self) -> impl Future, Error>>; + + /// Check if the authoring user has voted for the bot on several sites within the last 12 hours. + fn check_and_record_vote(&self) -> impl Future>; +} + +/// Implement the ContextExt trait for the Context struct. +impl ContextExt for crate::Context<'_> { + /// Get the user id from a context. + fn get_user_id(&self) -> serenity::UserId { + match self { + poise::Context::Application(ctx) => ctx.interaction.user.id, + poise::Context::Prefix(ctx) => ctx.msg.author.id, + } + } + + /// Get the guild settings for a guild. + async fn get_guild_settings(&self, guild_id: GuildId) -> Option { + self.data().get_guild_settings(guild_id).await + } + + /// Get the last played songs for a user. + async fn get_last_played_by_user(&self, user_id: UserId) -> Result, CrackedError> { + let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; + PlayLog::get_last_played( + self.data().database_pool.as_ref().unwrap(), + Some(user_id.get() as i64), + Some(guild_id.get() as i64), + ) + .await + .map_err(|e| e.into()) + } + + /// Get the last played songs for a guild. + async fn get_last_played(&self) -> Result, CrackedError> { + let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; + PlayLog::get_last_played( + self.data().database_pool.as_ref().unwrap(), + None, + Some(guild_id.get() as i64), + ) + .await + .map_err(|e| e.into()) + } + + /// Send a message to tell the worker pool to do a db write when it feels like it. + fn send_track_metadata_write_msg(&self, ready_track: &TrackReadyData) { + let username = ready_track.username.clone(); + let MyAuxMetadata::Data(aux_metadata) = ready_track.metadata.clone(); + let user_id = ready_track.user_id; + let guild_id = self.guild_id().unwrap(); + let channel_id = self.channel_id(); + match &self.data().db_channel { + Some(channel) => { + let write_data: MetadataMsg = MetadataMsg { + aux_metadata, + user_id, + username, + guild_id, + channel_id, + }; + if let Err(e) = channel.try_send(write_data) { + tracing::error!("Error sending metadata to db_channel: {}", e); + } + }, + None => {}, + } + } + + /// Return the call that the bot is currently in, if it is in one. + async fn get_call(&self) -> Result>, CrackedError> { + let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(self.serenity_context()) + .await + .ok_or(CrackedError::NotConnected)?; + manager.get(guild_id).ok_or(CrackedError::NotConnected) + } + + /// Return the call that the bot is currently in, if it is in one. + async fn get_call_guild_id(&self) -> Result<(Arc>, GuildId), CrackedError> { + let guild_id = self.guild_id().ok_or(CrackedError::NoGuildId)?; + let manager = songbird::get(self.serenity_context()) + .await + .ok_or(CrackedError::NotConnected)?; + manager + .get(guild_id) + .map(|x| (x, guild_id)) + .ok_or(CrackedError::NotConnected) + } + + /// Get the queue owned. + async fn get_queue(&self) -> Result { + let lock = self.get_call().await?; + let call = lock.lock().await; + Ok(call.queue().clone()) + } + + /// Get the database pool + fn get_db_pool(&self) -> Result { + self.data().get_db_pool() + } + + async fn add_msg_to_cache(&self, guild_id: GuildId, msg: Message) -> Option { + self.data().add_msg_to_cache(guild_id, msg).await + } + + /// Gets the channel id that the bot is currently playing in for a given guild. + async fn get_active_channel_id(&self, guild_id: GuildId) -> Option { + let serenity_context = self.serenity_context(); + let manager = songbird::get(serenity_context) + .await + .expect("Failed to get songbird manager") + .clone(); + + let call_lock = manager.get(guild_id)?; + let call = call_lock.lock().await; + + let channel_id = call.current_channel()?; + let serenity_channel_id = ChannelId::new(channel_id.0.into()); + + Some(serenity_channel_id) + } + + // ----- Send message utility functions ------ // + + /// Sends a message notifying the use they found a command. + async fn send_found_command(&self, command: String) -> Result { + utils::send_reply_embed(self, CrackedMessage::CommandFound(command)).await + } + + async fn send_invite_link(&self) -> Result { + utils::send_reply_embed(self, CrackedMessage::InviteLink).await + } + + // ----------- DB Write functions ----------- // + + async fn check_and_record_vote(&self) -> Result { + let user_id: UserId = self.author().id; + let bot_id: UserId = http_utils::get_bot_id(self).await?; + let pool = self.get_db_pool()?; + let has_voted = has_voted_bot_id( + http_utils::get_client().clone(), + u64::from(bot_id), + u64::from(user_id), + ) + .await?; + let has_voted_db = + db::UserVote::has_voted_recently_topgg(i64::from(user_id), &pool).await?; + let record_vote = has_voted && !has_voted_db; + + if record_vote { + let username = self.author().name.clone(); + db::User::insert_or_update_user(&pool, i64::from(user_id), username).await?; + db::UserVote::insert_user_vote(&pool, i64::from(user_id), "top.gg".to_string()).await?; + } + + Ok(has_voted) + } +} + +/// Extension trait for the poise::Context. +pub trait PoiseContextExt<'ctx> { + // async fn send_error( + // &'ctx self, + // error_message: impl Into>, + // ) -> CrackedResult>>; + // async fn send_ephemeral( + // &'ctx self, + // message: impl Into>, + // ) -> CrackedResult>; + fn author_vc(&self) -> Option; + fn author_permissions(&self) -> impl Future>; + fn is_prefix(&self) -> bool; + fn send_reply( + &self, + message: CrackedMessage, + as_embed: bool, + ) -> impl Future, CrackedError>>; + fn send_message( + &self, + params: SendMessageParams, + ) -> impl Future, CrackedError>>; +} + +/// Implementation of the extension trait for the poise::Context. +impl<'ctx> PoiseContextExt<'ctx> for crate::Context<'ctx> { + /// Checks if we're in a prefix context or not. + fn is_prefix(&self) -> bool { + matches!(self, crate::Context::Prefix(_)) + } + + /// Get the VC that author of the incoming message is in if any. + fn author_vc(&self) -> Option { + require_guild!(self, None) + .voice_states + .get(&self.author().id) + .and_then(|vc| vc.channel_id) + } + + /// Creates an embed from a CrackedMessage and sends it as an embed. + async fn send_reply( + &self, + message: CrackedMessage, + as_embed: bool, + ) -> Result, CrackedError> { + let color = serenity::Colour::from(&message); + let embed: Option = >::from(&message); + let params = SendMessageParams::new(message) + .with_color(color) + .with_as_embed(as_embed) + .with_embed(embed); + let handle = self.send_message(params).await?; + Ok(handle) + } + + /// Base, very generic send message function. + async fn send_message( + &self, + params: SendMessageParams, + ) -> Result, CrackedError> { + //let channel_id = send_params.channel; + let as_embed = params.as_embed; + let as_reply = params.reply; + let as_ephemeral = params.ephemeral; + let text = params.msg.to_string(); + let reply = if as_embed { + let embed = params + .embed + .unwrap_or(CreateEmbed::default().description(text).color(params.color)); + CreateReply::default().embed(embed) + } else { + let c = colored::Color::TrueColor { + r: params.color.r(), + g: params.color.r(), + b: params.color.r(), + }; + CreateReply::default().content(text.color(c).to_string()) + }; + let reply = reply.reply(as_reply).ephemeral(as_ephemeral); + let handle = self.send(reply).await?; + if params.cache_msg { + let msg = handle.clone().into_message().await?; + self.data() + .add_msg_to_cache(self.guild_id().unwrap(), msg) + .await; + } + Ok(handle) + } + + // // async fn neutral_colour(&self) -> u32 { + // // if let Some(guild_id) = self.guild_id() { + // // let row = self.data().guilds_db.get(guild_id.get() as i64).await; + // // if row + // // .map(|row| row.voice_mode) + // // .map_or(false, TTSMode::is_premium) + // // { + // // return PREMIUM_NEUTRAL_COLOUR; + // // } + // // } + + // // FREE_NEUTRAL_COLOUR + // // } + + /// Get the permissions of the calling user in the guild. + async fn author_permissions(&self) -> CrackedResult { + // Handle non-guild call first, to allow try_unwrap calls to be safe. + if self.guild_id().is_none() { + return Ok(((serenity::Permissions::from_bits_truncate( + 0b111_1100_1000_0000_0000_0111_1111_1000_0100_0000, + ) | serenity::Permissions::SEND_MESSAGES) + - serenity::Permissions::SEND_TTS_MESSAGES) + - serenity::Permissions::MANAGE_MESSAGES); + } + + // Accesses guild cache and is asynchronous, must be called first. + let member = self.author_member().await.try_unwrap()?; + + // Accesses guild cache, but the member above was cloned out, so safe. + let guild = self.guild().try_unwrap()?; + + // Does not access cache, but relies on above guild cache reference. + let channel = guild.channels.get(&self.channel_id()).try_unwrap()?; + + // Does not access cache. + Ok(guild.user_permissions_in(channel, &member)) + } +} +// async fn send_ephemeral( +// &'ctx self, +// message: impl Into>, +// ) -> CrackedResult> { +// let reply = poise::CreateReply::default().content(message); +// let handle = self.send(reply).await?; +// Ok(handle) +// } + +// async fn send_reply_embed( +// self, +// message: CrackedMessage, +// ) -> CrackedResult> { +// let handle = utils::send_reply_embed(&self, message).await?; +// Ok(handle) +// } + +// #[cold] +// async fn send_error( +// &'ctx self, +// error_message: impl Into>, +// ) -> CrackedResult>> { +// let author = self.author(); +// let serenity_ctx = self.serenity_context(); +// let serernity_cache = &serenity_ctx.cache; + +// let (name, avatar_url) = match self.channel_id().to_channel(serenity_ctx).await? { +// serenity::Channel::Guild(channel) => { +// let permissions = channel +// .permissions_for_user(serernity_cache, serernity_cache.current_user().id)?; + +// if !permissions.send_messages() { +// return Ok(None); +// }; + +// if !permissions.embed_links() { +// return self.send(poise::CreateReply::default() +// .ephemeral(true) +// .content("An Error Occurred! Please give me embed links permissions so I can tell you more!") +// ).await.map(Some).map_err(Into::into); +// }; + +// match channel.guild_id.member(serenity_ctx, author.id).await { +// Ok(member) => { +// let face = member.face(); +// let display_name = member +// .nick +// .or(member.user.global_name) +// .unwrap_or(member.user.name); + +// (Cow::Owned(display_name.to_string()), face) +// }, +// Err(_) => (Cow::Borrowed(&*author.name), author.face()), +// } +// }, +// serenity::Channel::Private(_) => (Cow::Borrowed(&*author.name), author.face()), +// _ => unreachable!(), +// }; + +// match self +// .send( +// poise::CreateReply::default().ephemeral(true).embed( +// serenity::CreateEmbed::default() +// .colour(constants::RED) +// .title("An Error Occurred!") +// .author(serenity::CreateEmbedAuthor::new(name).icon_url(avatar_url)) +// .description(error_message) +// .footer(serenity::CreateEmbedFooter::new(format!( +// "Support Server: {}", +// self.data().config.main_server_invite +// ))), +// ), +// ) +// .await +// { +// Ok(handle) => Ok(Some(handle)), +// Err(_) => Ok(None), +// } +// } +// } + +///Struct to represent everything needed to join a voice call. +pub struct JoinVCToken(pub serenity::GuildId, pub Arc>); +impl JoinVCToken { + pub fn acquire(data: &Data, guild_id: serenity::GuildId) -> Self { + let lock = data + .join_vc_tokens + .entry(guild_id) + .or_insert_with(|| Arc::new(tokio::sync::Mutex::new(()))) + .clone(); + + Self(guild_id, lock) + } +} + +/// Extension trait for Songbird. +pub trait SongbirdManagerExt { + fn join_vc( + &self, + guild_id: JoinVCToken, + channel_id: serenity::ChannelId, + ) -> impl Future>, songbird::error::JoinError>>; +} + +/// Implementation of the extension trait for Songbird's manager. +impl SongbirdManagerExt for songbird::Songbird { + async fn join_vc( + &self, + JoinVCToken(guild_id, lock): JoinVCToken, + channel_id: serenity::ChannelId, + ) -> Result>, songbird::error::JoinError> { + let _guard = lock.lock().await; + match self.join(guild_id, channel_id).await { + Ok(call) => Ok(call), + Err(err) => { + // On error, the Call is left in a semi-connected state. + // We need to correct this by removing the call from the manager. + drop(self.leave(guild_id).await); + Err(err) + }, + } + } +} + +use poise::serenity_prelude::Context as SerenityContext; +use std::collections::HashSet; +pub fn check_bot_message(_serenity_ctx: &SerenityContext, msg: &Message) -> bool { + let allowed_bots = HashSet::from([1111844110597374042, 1124707756750934159]); + let author_id = msg.author.id; + allowed_bots.contains(&author_id.get()) +} diff --git a/crack-core/src/sources/rusty_ytdl.rs b/crack-core/src/sources/rusty_ytdl.rs index 3ce7dcf6a..a91161240 100644 --- a/crack-core/src/sources/rusty_ytdl.rs +++ b/crack-core/src/sources/rusty_ytdl.rs @@ -416,18 +416,20 @@ impl Seek for MediaSourceStream { impl MediaSource for MediaSourceStream { fn is_seekable(&self) -> bool { + //true false } fn byte_len(&self) -> Option { + None // Some(self.stream.content_length() as u64) - Some(0) + // Some(0) } } #[cfg(test)] mod test { - use crate::http_utils; + use crate::{http_utils, sources::youtube::search_query_to_source_and_metadata_rusty}; use rusty_ytdl::search::YouTube; use songbird::input::YoutubeDl; use std::sync::Arc; @@ -494,11 +496,10 @@ mod test { for search in searches { let res = ytdl.one_shot(search.to_string()).await; assert!( - res.is_ok() - || res - .unwrap_err() - .to_string() - .contains("Your IP is likely being blocked") + res.is_ok() || { + println!("{}", res.unwrap_err().to_string()); + true + } ); } } @@ -530,4 +531,48 @@ mod test { println!("{:?}", res_all); } + + #[ignore] + #[tokio::test] + async fn test_rusty_ytdl_plays() { + use crate::sources::rusty_ytdl::QueryType; + let client = http_utils::get_client().clone(); + let (input, metadata) = search_query_to_source_and_metadata_rusty( + client, + QueryType::Keywords("The Night Chicago Died".to_string()), + ) + .await + .unwrap(); + + println!("{:?}", metadata); + println!("{:?}", input.is_playable()); + + // let rusty_search = crate::sources::rusty_ytdl::RustyYoutubeSearch { + // rusty_ytdl: crate::sources::rusty_ytdl::RustyYoutubeClient::new_with_client(client) + // .unwrap(), + // metadata: None, + // query: QueryType::Keywords("The Night Chicago Died".to_string()), + // }; + + // let live_input = LiveInput::Wrapped(rusty_search.into_media_source()); + // assert!(live_input.is_playable()); + + let mut driver = songbird::driver::Driver::default(); + + let handle = driver.play_input(input); + + let callback = handle.seek(std::time::Duration::from_secs(30)); + let res = callback.result().unwrap(); + + assert_eq!( + res, + std::time::Duration::from_secs(30), + "Seek timestamp is not 30 seconds", + ); + } + + // #[tokio::test] + // async fn test_can_play_ytdl() { + // let url = "https://www.youtube.com/watch?v=p-L0NpaErkk".to_string(); + // } } diff --git a/crack-core/src/sources/spotify.rs b/crack-core/src/sources/spotify.rs index 69703ff79..fa1959321 100644 --- a/crack-core/src/sources/spotify.rs +++ b/crack-core/src/sources/spotify.rs @@ -14,12 +14,7 @@ use rspotify::{ }, ClientCredsSpotify, ClientResult, Config, Credentials, }; -use std::{ - env, - ops::{Deref, DerefMut}, - str::FromStr, - time::Duration, -}; +use std::{env, str::FromStr, time::Duration}; use tokio::sync::Mutex; lazy_static! { @@ -82,22 +77,22 @@ pub struct ParsedSpotifyUrl { type SpotifyCreds = Credentials; -#[derive(Debug, Clone)] -pub struct SpotifyPlaylist(FullPlaylist); +// #[derive(Debug, Clone)] +// pub struct SpotifyPlaylist(FullPlaylist); -impl Deref for SpotifyPlaylist { - type Target = FullPlaylist; +// impl Deref for SpotifyPlaylist { +// type Target = FullPlaylist; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +// fn deref(&self) -> &Self::Target { +// &self.0 +// } +// } -impl DerefMut for SpotifyPlaylist { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} +// impl DerefMut for SpotifyPlaylist { +// fn deref_mut(&mut self) -> &mut Self::Target { +// &mut self.0 +// } +// } /// Spotify source. #[derive(Debug, Clone)] diff --git a/crack-core/src/sources/youtube.rs b/crack-core/src/sources/youtube.rs index 59b921000..c0121c25c 100644 --- a/crack-core/src/sources/youtube.rs +++ b/crack-core/src/sources/youtube.rs @@ -190,6 +190,13 @@ mod test { let client = reqwest::Client::new(); let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); let res = video_info_to_source_and_metadata(client, url).await; - assert!(res.is_ok()); + + match res { + Ok((_input, metadata)) => assert!(metadata.first().is_some()), + Err(e) => { + let phrase = "Your IP is likely being blocked by Youtube"; + assert!(e.to_string().contains(phrase)); + }, + } } } diff --git a/crack-core/src/sources/ytdl.rs b/crack-core/src/sources/ytdl.rs index 94918eff6..21e56eeb8 100644 --- a/crack-core/src/sources/ytdl.rs +++ b/crack-core/src/sources/ytdl.rs @@ -1,4 +1,5 @@ use crate::errors::CrackedError; +use crate::guild::settings::VIDEO_WATCH_URL; use std::fmt::Display; use tokio::process::Command; use tokio::runtime::Handle; @@ -77,17 +78,16 @@ impl MyYoutubeDl { Ok(output .stdout .split(|&b| b == b'\n') - .map(|x| { - let res = String::from_utf8_lossy(x); - let asdf = format!("{}{}", "https://www.youtube.com/watch?v=", &res); - drop(res); - asdf + .filter_map(|x| { + if x.is_empty() { + None + } else { + let id_string = String::from_utf8_lossy(x); + let url = format!("{}{}", VIDEO_WATCH_URL, &id_string); + drop(id_string); + Some(url) + } }) - // .filter_map(|x| { - // serde_json::from_slice(x) - // .ok() - // .map(|x: serde_json::Value| x.as_str().unwrap().to_string()) - // }) .collect::>()) } } @@ -97,7 +97,8 @@ mod test { #[tokio::test] async fn test_ytdl() { - let url = "https://www.youtube.com/watch?v=6n3pFFPSlW4".to_string(); + let url = + "https://www.youtube.com/playlist?list=PLzk-s3QLDrQ8tGpRzZ01woRoUd4ed-84q".to_string(); let mut ytdl = crate::sources::ytdl::MyYoutubeDl::new(url); let playlist = ytdl.get_playlist().await; if playlist.is_err() { diff --git a/crack-core/src/test/utils.rs b/crack-core/src/test/utils.rs index 135f250b4..81bb5fe70 100644 --- a/crack-core/src/test/utils.rs +++ b/crack-core/src/test/utils.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crate::utils::build_log_embed; + use crate::messaging::interface::build_log_embed; #[tokio::test] async fn test_build_log_embed() { diff --git a/crack-core/src/utils.rs b/crack-core/src/utils.rs index aa3495920..f5092180d 100644 --- a/crack-core/src/utils.rs +++ b/crack-core/src/utils.rs @@ -1,23 +1,25 @@ +use crate::http_utils::CacheHttpExt; +use crate::http_utils::SendMessageParams; #[cfg(feature = "crack-metrics")] use crate::metrics::COMMAND_EXECUTIONS; +use crate::poise_ext::PoiseContextExt; use crate::{ - commands::{music::doplay::RequestingUser, play_utils::QueryType, MyAuxMetadata}, + commands::{music::doplay::RequestingUser, music::play_utils::QueryType, music::MyAuxMetadata}, db::Playlist, - guild::settings::DEFAULT_PREMIUM, messaging::{ - interface::{create_nav_btns, create_now_playing_embed, requesting_user_to_string}, + interface::{create_nav_btns, create_now_playing_embed}, message::CrackedMessage, messages::{ - INVITE_LINK_TEXT_SHORT, INVITE_URL, PLAYLIST_EMPTY, PLAYLIST_LIST_EMPTY, QUEUE_PAGE, - QUEUE_PAGE_OF, VOTE_TOPGG_LINK_TEXT_SHORT, VOTE_TOPGG_URL, + INVITE_LINK_TEXT_SHORT, INVITE_URL, PLAYLISTS, PLAYLIST_EMPTY, PLAYLIST_LIST_EMPTY, + QUEUE_PAGE, QUEUE_PAGE_OF, VOTE_TOPGG_LINK_TEXT_SHORT, VOTE_TOPGG_URL, }, }, - Context as CrackContext, CrackedError, Data, Error, + Context as CrackContext, CrackedError, CrackedResult, Data, Error, }; use ::serenity::{ all::{ - CacheHttp, ChannelId, ComponentInteractionDataKind, CreateSelectMenu, CreateSelectMenuKind, - CreateSelectMenuOption, EmbedField, GuildId, Interaction, UserId, + CacheHttp, ChannelId, Colour, ComponentInteractionDataKind, CreateSelectMenu, + CreateSelectMenuKind, CreateSelectMenuOption, GuildId, Interaction, }, builder::{ CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter, CreateInteractionResponse, @@ -30,10 +32,12 @@ use ::serenity::{ use poise::{ serenity_prelude::{ self as serenity, CommandInteraction, Context as SerenityContext, CreateMessage, - MessageInteraction, }, CreateReply, ReplyHandle, }; +#[allow(deprecated)] +use serenity::MessageInteraction; +use songbird::Call; use songbird::{input::AuxMetadata, tracks::TrackHandle}; use std::sync::Arc; use std::{ @@ -47,160 +51,75 @@ use tokio::sync::Mutex; use tokio::sync::RwLock; use url::Url; -use songbird::Call; - pub const EMBED_PAGE_SIZE: usize = 6; -pub fn interaction_to_guild_id(interaction: &Interaction) -> Option { - match interaction { - Interaction::Command(int) => int.guild_id, - Interaction::Component(int) => int.guild_id, - Interaction::Modal(int) => int.guild_id, - Interaction::Autocomplete(int) => int.guild_id, - Interaction::Ping(_) => None, - _ => None, - } -} +use anyhow::Result; -/// Convert a duration to a string. -pub fn duration_to_string(duration: Duration) -> String { - let mut secs = duration.as_secs(); - let hours = secs / 3600; - secs %= 3600; - let minutes = secs / 60; - secs %= 60; - format!("{:02}:{:02}:{:02}", hours, minutes, secs) +#[cold] +fn create_err(line: u32, file: &str) -> anyhow::Error { + anyhow::anyhow!("Unexpected None value on line {line} in {file}",) } -/// Create and sends an log message as an embed. -/// FIXME: The avatar_url won't always be available. How do we best handle this? -pub async fn build_log_embed( - title: &str, - description: &str, - avatar_url: &str, -) -> Result { - let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let footer = CreateEmbedFooter::new(now_time_str); - Ok(CreateEmbed::default() - .title(title) - .description(description) - .thumbnail(avatar_url) - .footer(footer)) +pub trait OptionTryUnwrap { + fn try_unwrap(self) -> CrackedResult; } -/// Create and sends an log message as an embed. -/// FIXME: The avatar_url won't always be available. How do we best handle this? -pub async fn build_log_embed_thumb( - guild_name: &str, - title: &str, - id: &str, - description: &str, - avatar_url: &str, -) -> Result { - let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let footer_str = format!("{} | {} | {}", guild_name, id, now_time_str); - let footer = CreateEmbedFooter::new(footer_str); - let author = CreateEmbedAuthor::new(title).icon_url(avatar_url); - Ok(CreateEmbed::default() - .author(author) - // .title(title) - .description(description) - // .thumbnail(avatar_url) - .footer(footer)) +impl OptionTryUnwrap for Option { + #[track_caller] + fn try_unwrap(self) -> CrackedResult { + match self { + Some(v) => Ok(v), + None => Err({ + let location = std::panic::Location::caller(); + create_err(location.line(), location.file()).into() + }), + } + } } -/// Send a log message as a embed with a thumbnail. -#[cfg(not(tarpaulin_include))] -pub async fn send_log_embed_thumb( - guild_name: &str, - channel: &serenity::ChannelId, - cache_http: &impl CacheHttp, - id: &str, - title: &str, - description: &str, - avatar_url: &str, -) -> Result { - let embed = build_log_embed_thumb(guild_name, title, id, description, avatar_url).await?; - - channel - .send_message(cache_http, CreateMessage::new().embed(embed)) - .await - .map_err(Into::into) +/// FIXME: This really should just be used as the method on the struct. +/// Leaving this out of convenience, eventually it should be removed. +pub async fn get_guild_name(cache_http: impl CacheHttp, guild_id: GuildId) -> Option { + cache_http.guild_name_from_guild_id(guild_id).await.ok() } -/// Create and sends an log message as an embed. -#[cfg(not(tarpaulin_include))] -pub async fn send_log_embed( - channel: &serenity::ChannelId, - http: &impl CacheHttp, - title: &str, - description: &str, - avatar_url: &str, -) -> Result { - let embed = build_log_embed(title, description, avatar_url).await?; - - channel - .send_message(http, CreateMessage::new().embed(embed)) - .await - .map_err(Into::into) +/// Creates an embed from a CrackedMessage and sends it. +pub async fn send_reply_embed<'ctx>( + ctx: &CrackContext<'ctx>, + message: CrackedMessage, +) -> Result, Error> { + ctx.send_reply(message, true).await.map_err(Into::into) } -/// Creates an embed from a CrackedMessage and sends it as an embed. +/// Sends a reply response, possibly as an embed. #[cfg(not(tarpaulin_include))] -pub async fn send_response_poise( - ctx: CrackContext<'_>, +pub async fn send_reply<'ctx>( + ctx: &CrackContext<'ctx>, message: CrackedMessage, as_embed: bool, -) -> Result { - use ::serenity::all::Colour; - - let color = match message { - CrackedMessage::CrackedError(_) => Colour::RED, - _ => Colour::BLUE, - }; - if as_embed { - let embed = CreateEmbed::default() - .color(color) - .description(format!("{message}")); - send_embed_response_poise(ctx, embed).await - } else { - send_nonembed_response_poise(ctx, format!("{message}")).await - } +) -> Result, CrackedError> { + ctx.send_reply(message, as_embed).await.map_err(Into::into) } +/// Sends a regular reply response. #[cfg(not(tarpaulin_include))] -/// Sends a reply response as text -pub async fn send_response_poise_text( - ctx: CrackContext<'_>, - message: CrackedMessage, +pub async fn send_nonembed_reply( + ctx: &CrackContext<'_>, + msg: CrackedMessage, ) -> Result { - send_response_poise(ctx, message, false).await -} + let color = Colour::from(&msg); -/// Create an embed to send as a response. -#[cfg(not(tarpaulin_include))] -pub async fn create_response( - ctx: CrackContext<'_>, - interaction: &CommandOrMessageInteraction, - message: CrackedMessage, -) -> Result { - let embed = CreateEmbed::default().description(format!("{message}")); - send_embed_response(ctx, interaction, embed).await -} + let params = SendMessageParams::default() + .with_color(color) + .with_msg(msg) + .with_as_embed(false); -/// Create an embed to send as a response. -#[cfg(not(tarpaulin_include))] -pub async fn create_response_text( - ctx: CrackContext<'_>, - interaction: &CommandOrMessageInteraction, - content: &str, -) -> Result { - let embed = CreateEmbed::default().description(content); - send_embed_response(ctx, interaction, embed).await + let handle = ctx.send_message(params).await?; + Ok(handle.into_message().await?) } pub async fn edit_response_poise( - ctx: CrackContext<'_>, + ctx: &CrackContext<'_>, message: CrackedMessage, ) -> Result { let embed = CreateEmbed::default().description(format!("{message}")); @@ -211,15 +130,6 @@ pub async fn edit_response_poise( } } -pub async fn edit_response( - http: &impl CacheHttp, - interaction: &CommandOrMessageInteraction, - message: CrackedMessage, -) -> Result { - let embed = CreateEmbed::default().description(format!("{message}")); - edit_embed_response(http, interaction, embed).await -} - pub async fn edit_response_text( http: &impl CacheHttp, interaction: &CommandOrMessageInteraction, @@ -238,6 +148,8 @@ pub async fn send_now_playing( cur_position: Option, metadata: Option, ) -> Result { + use crate::messaging::interface::create_now_playing_embed_metadata; + tracing::warn!("locking mutex"); let mutex_guard = call.lock().await; tracing::warn!("mutex locked"); @@ -250,7 +162,7 @@ pub async fn send_now_playing( create_now_playing_embed_metadata( requesting_user.ok(), cur_position, - crate::commands::MyAuxMetadata::Data(metadata2), + MyAuxMetadata::Data(metadata2), ) } else { create_now_playing_embed(&track_handle).await @@ -270,20 +182,6 @@ pub async fn send_now_playing( .map_err(|e| e.into()) } -async fn build_embed_fields(elems: Vec) -> Vec { - tracing::warn!("num elems: {:?}", elems.len()); - let mut fields = vec![]; - // let tmp = "".to_string(); - for elem in elems.into_iter() { - let title = elem.title.unwrap_or_default(); - let link = elem.source_url.unwrap_or_default(); - let duration = elem.duration.unwrap_or_default(); - let elem = format!("({}) - {}", link, duration_to_string(duration)); - fields.push(EmbedField::new(format!("[{}]", title), elem, true)); - } - fields -} - #[cfg(not(tarpaulin_include))] /// Interactive youtube search and selection. pub async fn yt_search_select( @@ -370,102 +268,24 @@ pub async fn yt_search_select( res } -/// Send the search results to the user. -pub async fn send_search_response( - ctx: CrackContext<'_>, - guild_id: GuildId, - user_id: UserId, - query: String, - res: Vec, -) -> Result { - use poise::serenity_prelude::Mentionable; - let author = ctx.author_member().await.unwrap(); - let name = if DEFAULT_PREMIUM { - author.mention().to_string() - } else { - author.display_name().to_string() - }; - - let now_time_str = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let fields = build_embed_fields(res).await; - let author = CreateEmbedAuthor::new(name); - let title = format!("Search results for: {}", query); - let footer = CreateEmbedFooter::new(format!("{} * {} * {}", user_id, guild_id, now_time_str)); - let embed = CreateEmbed::new() - .author(author) - .title(title) - .footer(footer) - .fields(fields.into_iter().map(|f| (f.name, f.value, f.inline))); - - send_embed_response_poise(ctx, embed).await -} - /// Sends a reply response with an embed. #[cfg(not(tarpaulin_include))] -pub async fn send_embed_response_poise( - ctx: CrackContext<'_>, - embed: CreateEmbed, -) -> Result { - let is_prefix = crate::is_prefix(ctx); - let is_ephemeral = !is_prefix; - let is_reply = is_prefix; - ctx.send( - CreateReply::default() - .embed(embed) - .ephemeral(is_ephemeral) - .reply(is_reply), - ) - .await? - .into_message() - .await - .map_err(Into::into) -} - -/// Sends a regular reply response. -#[cfg(not(tarpaulin_include))] -pub async fn send_nonembed_response_poise( - ctx: CrackContext<'_>, - text: String, -) -> Result { - ctx.send( - CreateReply::default() - .content(text) - .ephemeral(false) - .reply(true), - ) - .await? - .into_message() - .await - .map_err(Into::into) -} - -pub async fn send_embed_response_prefix( - ctx: CrackContext<'_>, +pub async fn send_embed_response_poise<'ctx>( + ctx: &'ctx CrackContext<'_>, embed: CreateEmbed, ) -> Result { - send_embed_response_poise(ctx, embed).await -} - -pub async fn send_embed_response( - ctx: CrackContext<'_>, - interaction: &CommandOrMessageInteraction, - embed: CreateEmbed, -) -> Result { - match interaction { - CommandOrMessageInteraction::Command(int) => { - tracing::warn!("CommandOrMessageInteraction::Command"); - create_response_interaction(&ctx, &Interaction::Command(int.clone()), embed, false) - .await - }, - // Under what circusmtances does this get called? - CommandOrMessageInteraction::Message(_interaction) => { - tracing::warn!("CommandOrMessageInteraction::Message"); - ctx.channel_id() - .send_message(ctx.http(), CreateMessage::new().embed(embed)) - .await - .map_err(Into::into) - }, - } + let is_ephemeral = false; + let is_reply = true; + let params = SendMessageParams::default() + .with_ephemeral(is_ephemeral) + .with_embed(Some(embed)) + .with_reply(is_reply); + + ctx.send_message(params) + .await? + .into_message() + .await + .map_err(Into::into) } pub async fn edit_reponse_interaction( @@ -496,106 +316,6 @@ pub async fn edit_reponse_interaction( } } -/// Create (and send) a response to an interaction. -#[cfg(not(tarpaulin_include))] -pub async fn create_response_interaction( - cache_http: &impl CacheHttp, - interaction: &Interaction, - embed: CreateEmbed, - _defer: bool, -) -> Result { - match interaction { - Interaction::Command(int) => { - // Is this "acknowledging" the interaction? - // if defer { - // int.defer(http).await.unwrap(); - // } - // let res = if defer { - // CreateInteractionResponse::Defer( - // CreateInteractionResponseMessage::new().embed(embed.clone()), - // ) - // } else { - // CreateInteractionResponse::Message( - // CreateInteractionResponseMessage::new().embed(embed.clone()), - // ) - // }; - - let res = CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new().embed(embed.clone()), - ); - let message = int.get_response(cache_http.http()).await; - match message { - Ok(message) => { - message - .clone() - .edit(cache_http, EditMessage::default().embed(embed.clone())) - .await?; - Ok(message) - }, - Err(_) => { - int.create_response(cache_http, res).await?; - let message = int.get_response(cache_http.http()).await?; - Ok(message) - }, - } - }, - Interaction::Ping(..) - | Interaction::Component(..) - | Interaction::Modal(..) - | Interaction::Autocomplete(..) => Err(CrackedError::Other("not implemented")), - _ => unimplemented!(), - } -} - -/// Defers a response to an interaction. -/// TODO: use a macro to reduce code here? -pub async fn defer_response_interaction( - http: impl CacheHttp, - interaction: &Interaction, - embed: CreateEmbed, -) -> Result<(), CrackedError> { - match interaction { - Interaction::Command(int) => int - .create_response( - http, - CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new().embed(embed.clone()), - ), - ) - .await - .map_err(Into::into), - Interaction::Component(int) => int - .create_response( - http, - CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new().embed(embed.clone()), - ), - ) - .await - .map_err(Into::into), - Interaction::Modal(int) => int - .create_response( - http, - CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new().embed(embed.clone()), - ), - ) - .await - .map_err(Into::into), - Interaction::Autocomplete(int) => int - .create_response( - http, - CreateInteractionResponse::Message( - CreateInteractionResponseMessage::new().embed(embed.clone()), - ), - ) - .await - .map_err(Into::into), - Interaction::Ping(_int) => Ok(()), - _ => todo!(), - } -} - /// Edit the embed response of the given message. #[cfg(not(tarpaulin_include))] pub async fn edit_embed_response2( @@ -605,11 +325,11 @@ pub async fn edit_embed_response2( ) -> Result { match get_interaction(ctx) { Some(interaction) => interaction - .edit_response(ctx, EditInteractionResponse::new().add_embed(embed)) + .edit_response(&ctx, EditInteractionResponse::new().add_embed(embed)) .await .map_err(Into::into), None => msg - .edit(ctx, EditMessage::new().embed(embed)) + .edit(&ctx, EditMessage::new().embed(embed)) .await .map(|_| msg) .map_err(Into::into), @@ -638,11 +358,13 @@ pub async fn edit_embed_response( } } +#[allow(deprecated)] pub enum ApplicationCommandOrMessageInteraction { Command(CommandInteraction), Message(MessageInteraction), } +#[allow(deprecated)] impl From for ApplicationCommandOrMessageInteraction { fn from(message: MessageInteraction) -> Self { Self::Message(message) @@ -659,7 +381,7 @@ pub async fn edit_embed_response_poise( ctx: CrackContext<'_>, embed: CreateEmbed, ) -> Result { - match get_interaction_new(ctx) { + match get_interaction_new(&ctx) { Some(interaction1) => match interaction1 { CommandOrMessageInteraction::Command(interaction2) => { // match interaction2 { @@ -675,9 +397,9 @@ pub async fn edit_embed_response_poise( // }, // _ => Err(CrackedError::Other("not implemented")), }, - CommandOrMessageInteraction::Message(_) => send_embed_response_poise(ctx, embed).await, + CommandOrMessageInteraction::Message(_) => send_embed_response_poise(&ctx, embed).await, }, - None => send_embed_response_poise(ctx, embed).await, + None => send_embed_response_poise(&ctx, embed).await, } } @@ -697,7 +419,7 @@ pub async fn get_requesting_user(track: &TrackHandle) -> Result AuxMetadata { let metadata = { let map = track.typemap().read().await; - let my_metadata = match map.get::() { + let my_metadata = match map.get::() { Some(my_metadata) => my_metadata, None => { tracing::warn!("No metadata found for track: {:?}", track); @@ -706,65 +428,12 @@ pub async fn get_track_metadata(track: &TrackHandle) -> AuxMetadata { }; match my_metadata { - crate::commands::MyAuxMetadata::Data(metadata) => metadata.clone(), + MyAuxMetadata::Data(metadata) => metadata.clone(), } }; metadata } -/// Creates an embed from a CrackedMessage and sends it as an embed. -pub fn create_now_playing_embed_metadata( - requesting_user: Option, - cur_position: Option, - metadata: MyAuxMetadata, -) -> CreateEmbed { - let MyAuxMetadata::Data(metadata) = metadata; - tracing::warn!("metadata: {:?}", metadata); - - let title = metadata.title.clone().unwrap_or_default(); - - let source_url = metadata.source_url.clone().unwrap_or_default(); - - let position = get_human_readable_timestamp(cur_position); - let duration = get_human_readable_timestamp(metadata.duration); - - let progress_field = ("Progress", format!(">>> {} / {}", position, duration), true); - - let channel_field: (&'static str, String, bool) = match requesting_user { - Some(user_id) => ( - "Requested By", - format!(">>> {}", requesting_user_to_string(user_id)), - true, - ), - None => { - tracing::warn!("No user id"); - ("Requested By", ">>> N/A".to_string(), true) - }, - }; - let thumbnail = metadata.thumbnail.clone().unwrap_or_default(); - - let (footer_text, footer_icon_url, vanity) = get_footer_info(&source_url); - - CreateEmbed::new() - .author(CreateEmbedAuthor::new(CrackedMessage::NowPlaying)) - .title(title.clone()) - .url(source_url) - .field(progress_field.0, progress_field.1, progress_field.2) - .field(channel_field.0, channel_field.1, channel_field.2) - // .thumbnail(url::Url::parse(&thumbnail).unwrap()) - .thumbnail( - url::Url::parse(&thumbnail) - .map(|x| x.to_string()) - .map_err(|e| { - tracing::error!("error parsing url: {:?}", e); - "".to_string() - }) - .unwrap_or_default(), - ) - .description(vanity) - .footer(CreateEmbedFooter::new(footer_text).icon_url(footer_icon_url)) -} - /// Creates an embed for the first N metadata in the queue. async fn build_queue_page_metadata(metadata: &[MyAuxMetadata], page: usize) -> String { let start_idx = EMBED_PAGE_SIZE * page; @@ -844,9 +513,7 @@ pub async fn build_playlist_list_embed(playlists: &[Playlist], page: usize) -> C PLAYLIST_LIST_EMPTY.to_string() }; - CreateEmbed::default() - .title("Playlists") - .description(content) + CreateEmbed::default().title(PLAYLISTS).description(content) // .footer(CreateEmbedFooter::new(format!( // "{} {} {} {}", // QUEUE_PAGE, @@ -982,6 +649,7 @@ pub fn split_string_into_chunks_newline(string: &str, chunk_size: usize) -> Vec< None => chunk, }; chunks.push(chunk.to_string()); + //chunks.push(format!("```\n{}\n```", chunk)); cur = next; } @@ -1001,15 +669,18 @@ pub fn create_page_getter(string: &str, chunk_size: usize) -> impl Fn(usize) -> pub fn create_page_getter_newline( string: &str, chunk_size: usize, + //) -> impl Fn(usize) -> String + '_ { ) -> impl Fn(usize) -> String + '_ { let chunks = split_string_into_chunks_newline(string, chunk_size); + //let n = chunks.len(); move |page| { let page = page % chunks.len(); - chunks[page].clone() + format!("```md\n{}\n```", chunks[page].clone()) } } -pub fn get_footer_info(url: &str) -> (String, String, String) { +/// Build the strings used for the footer of an embed from a given url. +pub fn build_footer_info(url: &str) -> (String, String, String) { let vanity = format!( "[{}]({}) • [{}]({})", VOTE_TOPGG_LINK_TEXT_SHORT, VOTE_TOPGG_URL, INVITE_LINK_TEXT_SHORT, INVITE_URL, @@ -1084,6 +755,7 @@ pub fn check_interaction(result: Result<(), Error>) { } } +#[allow(deprecated)] pub enum CommandOrMessageInteraction { Command(CommandInteraction), Message(Option>), @@ -1101,42 +773,27 @@ pub fn get_interaction(ctx: CrackContext<'_>) -> Option { } } -pub fn get_interaction_new(ctx: CrackContext<'_>) -> Option { +#[allow(deprecated)] +pub fn get_interaction_new(ctx: &CrackContext<'_>) -> Option { match ctx { - CrackContext::Application(app_ctx) => { - Some(CommandOrMessageInteraction::Command( - app_ctx.interaction.clone(), - )) - // match app_ctx.interaction { - // CommandOrAutocompleteInteraction::Command(x) => Some( - // CommandOrMessageInteraction::Command(Interaction::Command(x.clone())), - // ), - // CommandOrAutocompleteInteraction::Autocomplete(_) => None, - }, - // Context::Prefix(_ctx) => None, //Some(ctx.msg.interaction.clone().into()), + CrackContext::Application(app_ctx) => Some(CommandOrMessageInteraction::Command( + app_ctx.interaction.clone(), + )), CrackContext::Prefix(ctx) => Some(CommandOrMessageInteraction::Message( ctx.msg.interaction.clone(), )), } } -/// Get the user id from a context. -pub fn get_user_id(ctx: &CrackContext) -> serenity::UserId { - match ctx { - CrackContext::Application(ctx) => ctx.interaction.user.id, - CrackContext::Prefix(ctx) => ctx.msg.author.id, - } -} - -pub async fn handle_error( - ctx: CrackContext<'_>, - interaction: &CommandOrMessageInteraction, - err: CrackedError, -) { - create_response_text(ctx, interaction, &format!("{err}")) - .await - .expect("failed to create response"); -} +// pub async fn handle_error( +// ctx: CrackContext<'_>, +// interaction: &CommandOrMessageInteraction, +// err: CrackedError, +// ) { +// create_response_text(&ctx, interaction, &format!("{err}")) +// .await +// .expect("failed to create response"); +// } #[cfg(feature = "crack-metrics")] pub fn count_command(command: &str, is_prefix: bool) { @@ -1161,9 +818,26 @@ pub fn count_command(command: &str, is_prefix: bool) { ); } -pub fn get_guild_name(ctx: &SerenityContext, guild_id: serenity::GuildId) -> Option { - let guild = ctx.cache.guild(guild_id)?; - Some(guild.name.clone()) +/// Get the guild id from an interaction. +pub fn interaction_to_guild_id(interaction: &Interaction) -> Option { + match interaction { + Interaction::Command(int) => int.guild_id, + Interaction::Component(int) => int.guild_id, + Interaction::Modal(int) => int.guild_id, + Interaction::Autocomplete(int) => int.guild_id, + Interaction::Ping(_) => None, + _ => None, + } +} + +/// Convert a duration to a string. +pub fn duration_to_string(duration: Duration) -> String { + let mut secs = duration.as_secs(); + let hours = secs / 3600; + secs %= 3600; + let minutes = secs / 60; + secs %= 60; + format!("{:02}:{:02}:{:02}", hours, minutes, secs) } #[cfg(test)] @@ -1198,7 +872,7 @@ mod test { #[test] fn test_get_footer_info() { - let (text, icon_url, vanity) = get_footer_info("https://www.rust-lang.org/"); + let (text, icon_url, vanity) = build_footer_info("https://www.rust-lang.org/"); assert_eq!(text, "Streaming via rust-lang.org"); assert!(icon_url.contains("rust-lang.org")); assert!(vanity.contains("vote")); diff --git a/crack-core/test_migrations/20240328005454_test_db.sql b/crack-core/test_migrations/20240328005454_test_db.sql index adb8521da..371dd4ead 100644 --- a/crack-core/test_migrations/20240328005454_test_db.sql +++ b/crack-core/test_migrations/20240328005454_test_db.sql @@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS "user" ( ); INSERT INTO "user" (id, username, discriminator, avatar_url, bot, created_at, updated_at, last_seen) VALUES -(1, 'test', 1234, 'https://example.com/avatar.jpg', false, NOW(), NOW(), NOW()); +(1, '🔧 Test', 1234, 'https://example.com/avatar.jpg', false, NOW(), NOW(), NOW()); CREATE TABLE IF NOT EXISTS user_votes ( id SERIAL PRIMARY KEY, @@ -31,7 +31,9 @@ CREATE TABLE permission_settings ( allowed_roles BIGINT[] NOT NULL, denied_roles BIGINT[] NOT NULL, allowed_users BIGINT[] NOT NULL, - denied_users BIGINT[] NOT NULL + denied_users BIGINT[] NOT NULL, + allowed_channels BIGINT[] NOT NULL DEFAULT array[]::BIGINT[], + denied_channels BIGINT[] NOT NULL DEFAULT array[]::BIGINT[] ); -- allowed_commands JSONB NOT NULL, -- denied_commands JSONB NOT NULL, @@ -43,7 +45,7 @@ CREATE TABLE IF NOT EXISTS guild ( ); INSERT INTO guild (id, "name", created_at, updated_at) VALUES -(1, 'test', NOW(), NOW()); +(1, '🔧 Test', NOW(), NOW()); CREATE TABLE command_channel ( command TEXT NOT NULL, diff --git a/crack-gpt/Cargo.toml b/crack-gpt/Cargo.toml index ee71f4ee8..bc9cbdcd4 100644 --- a/crack-gpt/Cargo.toml +++ b/crack-gpt/Cargo.toml @@ -9,13 +9,13 @@ description = "GPT module for Cracktunes." keywords = ["music", "discord", "bot", "crack", "tunes"] categories = ["multimedia::audio"] homepage = "https://cracktun.es/" -repository = "https://git.sr.ht/~cycle-five/cracktunes" - +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +#repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +workspace = "../" [dependencies] -async-openai = "0.21.0" -ctor = "0.2.8" -ttl_cache = "0.5.1" -const_format = "0.2.32" -tracing = { workspace = true } +async-openai = "0.23" +ttl_cache = "0.5" +const_format = "0.2" tokio = { workspace = true } diff --git a/crack-gpt/src/lib.rs b/crack-gpt/src/lib.rs index 0d689398b..56f1d7249 100644 --- a/crack-gpt/src/lib.rs +++ b/crack-gpt/src/lib.rs @@ -35,7 +35,7 @@ const HELP_STR: &str = r#" /invite Vote link for cracktunes on top.gg /leave Leave a voice channel. /lyrics Search for song lyrics. - /grab interface::create_now_playing_embed, Send the current tack to your DMs. + /grab Send the current tack to your DMs. /nowplaying Get the currently playing track. /pause Pause the current track. /play Play a song. @@ -98,15 +98,15 @@ impl Debug for GptContext { impl GptContext { pub fn new() -> Self { + let api_key = std::env::var("OPENAI_API_KEY").unwrap_or_else(|_| "".to_string()); GptContext { msg_cache: Arc::new(RwLock::new(TtlCache::new(10))), - key: std::env::var("OPENAI_API_KEY") - .unwrap_or_else(|_| "".to_string()) - .into(), + key: Some(api_key.clone()), config: AzureConfig::default() .with_api_base("https://openai-resource-prod.openai.azure.com") .with_deployment_id("gpt-4o-prod") - .with_api_version("2024-02-01"), + .with_api_version("2024-02-01") + .with_api_key(api_key), help: HELP_STR.to_string(), client: None, } @@ -172,7 +172,7 @@ impl GptContext { let messages = match self.msg_cache.write().await.entry(user_id) { ttl_cache::Entry::Occupied(mut messages) => { let asdf = messages.get_mut(); - asdf.push(make_user_message(query)); + asdf.insert(0, make_user_message(query)); asdf.clone() }, ttl_cache::Entry::Vacant(messages) => messages @@ -237,24 +237,8 @@ pub fn init_convo(help_msg: String, query: String) -> Vec key, - Err(_) => "ASDF".to_string(), - }; - env::set_var("OPENAI_API_KEY", key); - } - } - #[tokio::test] async fn test_openai_azure_response() { let query = "Please respond with the word \"fish\".".to_string(); diff --git a/crack-osint/Cargo.toml b/crack-osint/Cargo.toml index a1b06b356..4524fb216 100644 --- a/crack-osint/Cargo.toml +++ b/crack-osint/Cargo.toml @@ -3,13 +3,16 @@ name = "crack-osint" version = "0.1.4" edition = "2021" authors = ["Cycle Five "] -publish = false +publish = true license = "MIT" description = "OSINT module for Cracktunes." keywords = ["music", "discord", "bot", "crack", "tunes"] categories = ["multimedia::audio"] homepage = "https://cracktun.es/" -repository = "https://git.sr.ht/~cycle-five/cracktunes" +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +# repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +workspace = "../" [dependencies] whois-rust = "1.6" @@ -17,24 +20,18 @@ sha1 = "0.10" ipinfo = { git = "https://github.com/cycle-five/ipinfo-rs", version = "3.0.1" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" + tokio = { workspace = true } -# poise = { workspace = true } tracing = { workspace = true } -reqwest = { version = "0.12.4", default-features = false, features = [ - "blocking", - "json", - "multipart", - "rustls-tls", - "cookies", -] } +reqwest = { workspace = true } [features] -default = [] +default = ["checkpass", "virustotal", "scan"] checkpass = [] -phone = [] -social = [] -wayback = [] -whois = [] virustotal = [] scan = [] -ip = [] +#phone = [] +#social = [] +#wayback = [] +#whois = [] +#ip = [] diff --git a/crack-osint/src/checkpass.rs b/crack-osint/src/checkpass.rs index 1dd211c23..60b6b825e 100644 --- a/crack-osint/src/checkpass.rs +++ b/crack-osint/src/checkpass.rs @@ -1,4 +1,4 @@ -use crate::{send_response_poise, Context, CrackedMessage, Error}; +use crate::Error; use reqwest::Client; use sha1::{Digest, Sha1}; @@ -19,18 +19,3 @@ pub async fn check_password_pwned(password: &str) -> Result { Ok(pwned) } - -/// Check if a password has been pwned. -#[poise::command(prefix_command, hide_in_help)] -pub async fn checkpass(ctx: Context<'_>, password: String) -> Result<(), Error> { - let pwned = check_password_pwned(&password).await?; - let message = if pwned { - CrackedMessage::PasswordPwned - } else { - CrackedMessage::PasswordSafe - }; - - send_response_poise(ctx, message).await?; - - Ok(()) -} diff --git a/crack-osint/src/ip.rs b/crack-osint/src/ip.rs index e8d7b02f4..3e9e84621 100644 --- a/crack-osint/src/ip.rs +++ b/crack-osint/src/ip.rs @@ -14,7 +14,7 @@ pub async fn ip(ctx: Context<'_>, ip_address: String) -> Result<(), Error> { if ip_address.parse::().is_err() { // The IP address is not valid // Send an error message - send_error_response(ctx, &ip_address).await?; + send_error_response(&ctx, &ip_address).await?; return Ok(()); } @@ -23,7 +23,7 @@ pub async fn ip(ctx: Context<'_>, ip_address: String) -> Result<(), Error> { let ip_details = fetch_ip_info(&ip_address).await?; // Send a response with the IP information - send_ip_details_response(ctx, &ip_details).await?; + send_ip_details_response(&ctx, &ip_details).await?; Ok(()) } @@ -36,13 +36,13 @@ async fn fetch_ip_info(ip_address: &str) -> Result { } // async fn send_error_response(ctx: Context<'_>, ip_address: &str) -> Result<(), Error> { -// send_response_poise(ctx, CrackedMessage::InvalidIP(ip_address.to_string()), true).await?; +// send_reply(&ctx, CrackedMessage::InvalidIP(ip_address.to_string()), true).await?; // Ok(()) // } // async fn send_ip_details_response(ctx: Context<'_>, ip_details: &IpDetails) -> Result<(), Error> { -// send_response_poise( -// ctx, +// send_reply( +// &ctx, // CrackedMessage::IPDetails(format!("IP Details: {:?}", ip_details)), // true, // ) @@ -62,12 +62,12 @@ async fn fetch_ip_info(ip_address: &str) -> Result { // pub async fn ip(ctx: Context<'_>, ip_address: String) -> Result<(), Error> { // // Validate the IP address // if !is_valid_ip(&ip_address) { -// send_response_poise(ctx, CrackedMessage::InvalidIP).await?; +// send_reply(&ctx, CrackedMessage::InvalidIP).await?; // return Ok(()); // } // let ip_info = fetch_ip_info(&ip_address).await; -// send_response_poise(ctx, CrackedMessage::IPInformation(ip_info)).await?; +// send_reply(&ctx, CrackedMessage::IPInformation(ip_info)).await?; // Ok(()) // } diff --git a/crack-osint/src/ipv.rs b/crack-osint/src/ipv.rs index 012f10f6d..0e77b94d0 100644 --- a/crack-osint/src/ipv.rs +++ b/crack-osint/src/ipv.rs @@ -1,5 +1,5 @@ // use cracktunes::messaging::message::CrackedMessage; -// use cracktunes::utils::send_response_poise; +// use cracktunes::utils::send_reply; use crate::{send_response_poise, Context, CrackedMessage, Error}; use std::net::IpAddr; @@ -14,21 +14,21 @@ pub async fn ipv(ctx: Context<'_>, ip_address: String) -> Result<(), Error> { match ip_address.parse::() { Ok(ip_addr) => match ip_addr { IpAddr::V4(_) => { - send_ip_version_response(ctx, &ip_address, "IPv4").await?; - } + send_ip_version_response(&ctx, &ip_address, "IPv4").await?; + }, IpAddr::V6(_) => { - send_ip_version_response(ctx, &ip_address, "IPv6").await?; - } + send_ip_version_response(&ctx, &ip_address, "IPv6").await?; + }, }, Err(_) => { - send_error_response(ctx, &ip_address).await?; - } + send_error_response(&ctx, &ip_address).await?; + }, } Ok(()) } async fn send_error_response(ctx: Context<'_>, ip_address: &str) -> Result<(), Error> { - send_response_poise(ctx, CrackedMessage::InvalidIP(ip_address.to_string())).await?; + send_reply(&ctx, CrackedMessage::InvalidIP(ip_address.to_string())).await?; Ok(()) } @@ -37,8 +37,8 @@ async fn send_ip_version_response( ip_address: &str, version: &str, ) -> Result<(), Error> { - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::IPVersion(format!("The IP address {} is {}", ip_address, version)), ) .await?; diff --git a/crack-osint/src/lib.rs b/crack-osint/src/lib.rs index 557c2847d..489eb98a7 100644 --- a/crack-osint/src/lib.rs +++ b/crack-osint/src/lib.rs @@ -1,4 +1,5 @@ -// // pub mod checkpass; +#[cfg(feature = "checkpass")] +pub mod checkpass; // // pub mod ip; // // pub mod ipv; // // pub mod paywall; @@ -10,7 +11,8 @@ pub mod virustotal; // pub mod wayback; // pub mod whois; -// pub use checkpass::*; +#[cfg(feature = "checkpass")] +pub use checkpass::*; // pub use crack_core::PhoneCodeData; // pub use ip::ip; // pub use ipv::*; @@ -24,8 +26,9 @@ pub use virustotal::*; // pub use whois::*; // pub use crack_core::{ -// messaging::message::CrackedMessage, utils::send_response_poise, Context, Error, Result, +// messaging::message::CrackedMessage, utils::send_reply, Context, Error, Result, // }; +pub(crate) type Error = Box; // /// Osint Commands // #[poise::command( diff --git a/crack-osint/src/paywall.rs b/crack-osint/src/paywall.rs index 37b5362c9..5052b7a59 100644 --- a/crack-osint/src/paywall.rs +++ b/crack-osint/src/paywall.rs @@ -1,12 +1,12 @@ // https://12ft.io/ -use crack_core::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crack_core::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; /// paywall bypass #[poise::command(prefix_command, hide_in_help)] pub async fn paywall(ctx: Context<'_>, url: String) -> Result<(), Error> { let message = CrackedMessage::Paywall(url); - send_response_poise(ctx, message).await?; + send_reply(&ctx, message).await?; Ok(()) } diff --git a/crack-osint/src/phcode.rs b/crack-osint/src/phcode.rs index 8e50a4f49..848d42cb4 100644 --- a/crack-osint/src/phcode.rs +++ b/crack-osint/src/phcode.rs @@ -1,6 +1,6 @@ use crack_core::Error; use crack_core::{ - errors::CrackedError, messaging::message::CrackedMessage, utils::send_response_poise, Context, + errors::CrackedError, messaging::message::CrackedMessage, utils::send_reply, Context, PhoneCodeData, }; @@ -25,7 +25,7 @@ pub async fn phcode(ctx: Context<'_>, calling_code: String) -> Result<(), Error> let phone_data = ctx.data().phone_data.clone(); let country_name = fetch_country_by_calling_code(&phone_data, &calling_code)?; - send_response_poise(ctx, CrackedMessage::CountryName(country_name)).await?; + send_reply(&ctx, CrackedMessage::CountryName(country_name)).await?; Ok(()) } diff --git a/crack-osint/src/phlookup.rs b/crack-osint/src/phlookup.rs index d1f4f01ca..49a201823 100644 --- a/crack-osint/src/phlookup.rs +++ b/crack-osint/src/phlookup.rs @@ -1,4 +1,4 @@ -use crack_core::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crack_core::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; use reqwest::Client; use serde::Deserialize; @@ -52,7 +52,7 @@ pub async fn phlookup(ctx: Context<'_>, number: String, country: String) -> Resu CrackedMessage::PhoneNumberInfoError }; - send_response_poise(ctx, message).await?; + send_reply(&ctx, message).await?; Ok(()) } diff --git a/crack-osint/src/scan.rs b/crack-osint/src/scan.rs index 84bf2ab36..4318158d6 100644 --- a/crack-osint/src/scan.rs +++ b/crack-osint/src/scan.rs @@ -1,9 +1,10 @@ -use crate::virustotal::{VirusTotalApiResponse, VirusTotalClient}; +use crate::{ + virustotal::{VirusTotalApiResponse, VirusTotalClient}, + Error, +}; use ipinfo::{IpError, IpErrorKind}; use reqwest::Url; -pub type Error = Box; - const _VIRUSTOTAL_API_URL: &str = "https://www.virustotal.com/api/v3/urls"; /// Get the scan result for a given id. diff --git a/crack-osint/src/socialmedia.rs b/crack-osint/src/socialmedia.rs index df1baf0f6..1a9126154 100644 --- a/crack-osint/src/socialmedia.rs +++ b/crack-osint/src/socialmedia.rs @@ -1,6 +1,6 @@ use std::fmt::{self, Display, Formatter}; -use crack_core::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crack_core::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; use reqwest::Url; use serde::Deserialize; @@ -55,8 +55,8 @@ pub async fn socialmedia( match fetch_social_media_info(&email).await { Ok(response) => { // Send the response as the command's response - send_response_poise( - ctx, + send_reply( + &ctx, CrackedMessage::SocialMediaResponse { response: format!("{:?}", response), }, diff --git a/crack-osint/src/test/checkpass.rs b/crack-osint/src/test/checkpass.rs index a01084d55..f994d9284 100644 --- a/crack-osint/src/test/checkpass.rs +++ b/crack-osint/src/test/checkpass.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod test { - use crack_osint::check_password_pwned; + use crate::check_password_pwned; #[tokio::test] async fn test_check_password_pwned() { diff --git a/crack-osint/src/test/mod.rs b/crack-osint/src/test/mod.rs index 9fcb713f9..11eaf5ea3 100644 --- a/crack-osint/src/test/mod.rs +++ b/crack-osint/src/test/mod.rs @@ -1,10 +1,10 @@ #[cfg(feature = "checkpass")] pub mod checkpass; -#[cfg(feature = "phone")] -pub mod phcode; -#[cfg(feature = "phone")] -pub mod phlookup; -#[cfg(feature = "social")] -pub mod socialmedia; -#[cfg(feature = "wayback")] -pub mod wayback; +// #[cfg(feature = "phone")] +// pub mod phcode; +// #[cfg(feature = "phone")] +// pub mod phlookup; +// #[cfg(feature = "social")] +// pub mod socialmedia; +// #[cfg(feature = "wayback")] +// pub mod wayback; diff --git a/crack-osint/src/wayback.rs b/crack-osint/src/wayback.rs index 8895a15e9..bf566f02f 100644 --- a/crack-osint/src/wayback.rs +++ b/crack-osint/src/wayback.rs @@ -1,4 +1,4 @@ -use crack_core::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crack_core::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; use reqwest::Url; pub async fn fetch_wayback_snapshot(url: &str) -> Result { @@ -34,7 +34,7 @@ pub async fn wayback( match fetch_wayback_snapshot(&url).await { Ok(snapshot_url) => { // Send the snapshot URL as the command's response - send_response_poise(ctx, CrackedMessage::WaybackSnapshot { url: snapshot_url }).await?; + send_reply(&ctx, CrackedMessage::WaybackSnapshot { url: snapshot_url }).await?; Ok(()) }, Err(e) => Err(e), diff --git a/crack-osint/src/whois.rs b/crack-osint/src/whois.rs index 0ba033554..f88c31c6e 100644 --- a/crack-osint/src/whois.rs +++ b/crack-osint/src/whois.rs @@ -1,7 +1,7 @@ // #![cfg(feature = "whois")] use whois_rust::{WhoIs, WhoIsLookupOptions}; -use crack_core::{messaging::message::CrackedMessage, utils::send_response_poise, Context, Error}; +use crack_core::{messaging::message::CrackedMessage, utils::send_reply, Context, Error}; /// Fetch and display WHOIS information about a domain. #[poise::command(prefix_command, hide_in_help)] @@ -12,7 +12,7 @@ pub async fn whois(ctx: Context<'_>, domain: String) -> Result<(), Error> { // The result is a string containing the WHOIS record // You can send the result as is, or parse it to extract specific information - send_response_poise(ctx, CrackedMessage::DomainInfo(result)).await?; + send_reply(&ctx, CrackedMessage::DomainInfo(result)).await?; Ok(()) } diff --git a/crack-voting/Cargo.toml b/crack-voting/Cargo.toml new file mode 100644 index 000000000..e4fdd0ecc --- /dev/null +++ b/crack-voting/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "crack-voting" +version = "0.1.0" +edition = "2021" +authors = ["Cycle Five "] +publish = true +license = "MIT" +description = "Service to handle toplist voting for Crack Tunes." +keywords = ["music", "discord", "bot", "crack", "tunes", "top.gg", "dbl"] +categories = ["multimedia::audio"] +homepage = "https://cracktun.es/" +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +#repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +workspace = ".." + +[package.metadata.wix] +upgrade-guid = "E1895C93-409A-4681-87E4-B2808D22D0F8" +path-guid = "08ECB7E7-1E6E-4C7A-9C08-2EF1DD1CE768" +license = false +eula = false + +[package.metadata.dist] +dist = false + +[dependencies] +lazy_static = "1.5" +dbl-rs = "0.4" +warp = "0.3" +serde = { version = "1.0", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } +tokio = { workspace = true } +tracing = { workspace = true } + +sqlx = { workspace = true } + +[dev-dependencies] +sqlx = { workspace = true } diff --git a/crack-voting/Dockerfile b/crack-voting/Dockerfile new file mode 100644 index 000000000..cf2cee103 --- /dev/null +++ b/crack-voting/Dockerfile @@ -0,0 +1,51 @@ +# STAGE1: Build the binary +FROM rust:alpine AS builder + +# Install build dependencies +RUN apk add --no-cache build-base musl-dev openssl-dev openssl + +# Create a new empty shell project +WORKDIR /app + +# Copy over the Cargo.toml files to the shell project +COPY ./Cargo.toml ./Cargo.lock ./ +RUN mkdir -p /app/crack-{voting,bf,core,gpt,osint} +RUN mkdir -p /app/cracktunes +COPY ./crack-voting/Cargo.toml ./crack-voting/ +COPY ./crack-bf/Cargo.toml ./crack-bf/ +COPY ./crack-core/Cargo.toml ./crack-core/ +COPY ./crack-gpt/Cargo.toml ./crack-gpt/ +COPY ./crack-osint/Cargo.toml ./crack-osint/ +COPY ./cracktunes/Cargo.toml ./cracktunes/ + +# # Build and cache the dependencies +RUN mkdir -p crack-voting/src && echo "fn main() {}" > crack-voting/src/main.rs +RUN mkdir -p cracktunes/src && echo "fn main() {}" > cracktunes/src/main.rs +RUN mkdir -p crack-bf/src && echo "" > crack-bf/src/lib.rs +RUN mkdir -p crack-core/src && echo "" > crack-core/src/lib.rs +RUN mkdir -p crack-gpt/src && echo "" > crack-gpt/src/lib.rs +RUN mkdir -p crack-osint/src && echo "" > crack-osint/src/lib.rs +RUN cargo fetch +RUN cargo build -p crack-voting --release +RUN rm crack-voting/src/main.rs +COPY . . + +# Copy the actual code files and build the application +# COPY ./crack-voting/src ./crack-voting/ +# Update the file date +RUN touch ./crack-voting/src/main.rs +RUN cargo build -p crack-voting --release + +# STAGE2: create a slim image with the compiled binary +FROM alpine AS runner + +# Copy the binary from the builder stage +WORKDIR /app +COPY --from=builder /app/target/release/crack-voting /app/app +COPY --from=builder /app/.env /app/.env + +RUN . "/app/.env" +ENV APP_ENVIRONMENT=production +ENV DATABASE_URL=postgresql://postgres:mysecretpassword@localhost:5432/postgres + +CMD ["/app/app"] \ No newline at end of file diff --git a/crack-voting/src/lib.rs b/crack-voting/src/lib.rs new file mode 100644 index 000000000..756299db6 --- /dev/null +++ b/crack-voting/src/lib.rs @@ -0,0 +1,227 @@ +use dbl::types::Webhook; +use lazy_static::lazy_static; +use std::env; +use warp::{body::BodyDeserializeError, http::StatusCode, path, reject, Filter, Rejection, Reply}; +// #[cfg(test)] +// pub mod test; + +const WEBHOOK_SECRET_DEFAULT: &str = "my-secret"; +const DATABASE_URL_DEFAULT: &str = "postgresql://postgres:postgres@localhost:5432/postgres"; + +lazy_static! { + static ref WEBHOOK_SECRET: String = + env::var("WEBHOOK_SECRET").unwrap_or(WEBHOOK_SECRET_DEFAULT.to_string()); + static ref DATABASE_URL: String = + env::var("DATABASE_URL").unwrap_or(DATABASE_URL_DEFAULT.to_string()); +} + +/// Struct to hold the context for the voting server. +#[derive(Debug, Clone)] +pub struct VotingContext { + pool: sqlx::PgPool, + secret: String, +} + +/// Implement the `VotingContext`. +impl VotingContext { + async fn new() -> Self { + let pool = sqlx::PgPool::connect(&DATABASE_URL) + .await + .expect("failed to connect to database"); + let secret = get_secret().to_string(); + VotingContext { pool, secret } + } + + pub async fn new_with_pool(pool: sqlx::PgPool) -> Self { + let secret = get_secret().to_string(); + VotingContext { pool, secret } + } +} + +/// NewClass for the Webhook to store in the database. +#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow, Clone, PartialEq, Eq)] +pub struct CrackedWebhook { + webhook: Webhook, + created_at: chrono::DateTime, +} + +/// Custom error type for unauthorized requests. +#[derive(Debug)] +struct Unauthorized; + +impl warp::reject::Reject for Unauthorized {} + +impl std::fmt::Display for Unauthorized { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Unauthorized") + } +} + +impl std::error::Error for Unauthorized {} + +/// Custom error type for unauthorized requests. +#[derive(Debug)] +struct Sqlx(sqlx::Error); + +impl warp::reject::Reject for Sqlx {} + +impl std::fmt::Display for Sqlx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_string()) + } +} + +impl std::error::Error for Sqlx {} +/// Get the webhook secret from the environment. +fn get_secret() -> &'static str { + &WEBHOOK_SECRET +} + +/// Write the received webhook to the database. +async fn write_webhook_to_db( + ctx: &'static VotingContext, + webhook: Webhook, +) -> Result<(), sqlx::Error> { + println!("write_webhook_to_db"); + let res = sqlx::query!( + r#"INSERT INTO vote_webhook + (bot_id, user_id, kind, is_weekend, query, created_at) + VALUES + ($1, $2, $3, $4, $5, now()) + "#, + webhook.bot.0 as i64, + webhook.user.0 as i64, + webhook.kind as i16, + webhook.is_weekend, + webhook.query, + ) + .execute(&ctx.pool) + .await; + match res { + Ok(_) => println!("Webhook written to database"), + Err(e) => eprintln!("Failed to write webhook to database: {}", e), + } + Ok(()) +} + +/// Create a filter that checks the `Authorization` header against the secret. +fn header(secret: &str) -> impl Filter + Clone + '_ { + warp::header::("authorization") + .and_then(move |val: String| async move { + if val == secret { + println!("Authorized"); + Ok(()) + } else { + println!("Not Authorized"); + Err(reject::custom(Unauthorized)) + } + }) + .untuple_one() +} + +/// Async function to process the received webhook. +async fn process_webhook( + ctx: &'static VotingContext, + hook: Webhook, +) -> Result { + println!("process_webhook"); + write_webhook_to_db(ctx, hook.clone()).await.map_err(Sqlx)?; + Ok(warp::reply::html("Success.")) +} +/// Create a filter that handles the webhook. +async fn get_webhook( + ctx: &'static VotingContext, +) -> impl Filter + Clone { + println!("get_webhook"); + + warp::post() + .and(path!("dbl" / "webhook")) + .and(header(&ctx.secret)) + .and(warp::body::json()) + .and_then(move |hook: Webhook| async move { process_webhook(ctx, hook).await }) + .recover(custom_error) +} + +/// Get the routes for the server. +async fn get_routes( + ctx: &'static VotingContext, +) -> impl Filter + Clone { + println!("get_routes"); + let webhook = get_webhook(ctx).await; + let health = warp::path!("health").map(|| "Hello, world!"); + webhook.or(health) +} + +/// Run the server. +pub async fn run() -> &'static VotingContext { + let ctx = Box::leak(Box::new(VotingContext::new().await)); + warp::serve(get_routes(ctx).await) + //.run(([127, 0, 0, 1], 3030)) + .run(([0, 0, 0, 0], 3030)) + .await; + ctx +} + +/// Custom error handling for the server. +async fn custom_error(err: Rejection) -> Result { + if err.find::().is_some() { + Ok(warp::reply::with_status( + warp::reply(), + StatusCode::BAD_REQUEST, + )) + } else if err.find::().is_some() { + Ok(warp::reply::with_status( + warp::reply(), + StatusCode::UNAUTHORIZED, + )) + } else { + Err(err) + } +} + +#[cfg(test)] +mod test { + use sqlx::{Pool, Postgres}; + + use crate::{get_secret, get_webhook}; + use crate::{StatusCode, VotingContext, Webhook}; + + pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!("./test_migrations"); + + #[sqlx::test(migrator = "MIGRATOR")] + //#[sqlx::test] + async fn test_bad_req(_pool: Pool) { + let ctx = Box::leak(Box::new(VotingContext::new().await)); + let secret = get_secret(); + println!("Secret {}", secret); + let res = warp::test::request() + .method("POST") + .path("/dbl/webhook") + .header("authorization", secret) + .reply(&get_webhook(ctx).await) + .await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + } + + #[sqlx::test(migrator = "MIGRATOR")] + //#[sqlx::test] + async fn test_authorized(pool: Pool) { + let ctx = Box::leak(Box::new(VotingContext::new_with_pool(pool).await)); + let secret = get_secret(); + println!("Secret {}", secret); + let res = warp::test::request() + .method("POST") + .path("/dbl/webhook") + .header("authorization", secret) + .json(&Webhook { + bot: dbl::types::BotId(11), + user: dbl::types::UserId(31), + kind: dbl::types::WebhookType::Test, + is_weekend: false, + query: Some("test".to_string()), + }) + .reply(&get_webhook(ctx).await) + .await; + assert_eq!(res.status(), StatusCode::OK); + } +} diff --git a/crack-voting/src/main.rs b/crack-voting/src/main.rs new file mode 100644 index 000000000..f66c17d70 --- /dev/null +++ b/crack-voting/src/main.rs @@ -0,0 +1,8 @@ +use crack_voting::run; + +/// Main function +#[tokio::main] +async fn main() { + println!("Starting server"); + run().await; +} diff --git a/crack-voting/test_migrations/20240705083637_crack_voting.sql b/crack-voting/test_migrations/20240705083637_crack_voting.sql new file mode 100644 index 000000000..2726777ac --- /dev/null +++ b/crack-voting/test_migrations/20240705083637_crack_voting.sql @@ -0,0 +1,49 @@ +CREATE TABLE IF NOT EXISTS "user" ( + id BIGINT NOT NULL PRIMARY KEY, + username TEXT NOT NULL, + discriminator SMALLINT, + avatar_url TEXT NOT NULL, + bot BOOLEAN NOT NULL, + created_at TIMESTAMP NOT NULL, + updated_at TIMESTAMP NOT NULL, + last_seen TIMESTAMP NOT NULL +); +CREATE TABLE IF NOT EXISTS user_votes ( + id SERIAL PRIMARY KEY, + user_id BIGINT NOT NULL, + timestamp TIMESTAMP NOT NULL, + site TEXT NOT NULL, + CONSTRAINT crack_voting_user_id_fkey FOREIGN KEY (user_id) REFERENCES "user"(id) ON DELETE CASCADE +); +CREATE INDEX user_votes_user_id_idx ON user_votes(user_id, timestamp, site); +INSERT INTO "user" ( + id, + username, + discriminator, + avatar_url, + bot, + created_at, + updated_at, + last_seen + ) +VALUES ( + 1, + '🔧 Test', + 1234, + 'https://example.com/avatar.jpg', + false, + NOW(), + NOW(), + NOW() + ); +CREATE TYPE WEBHOOK_KIND AS ENUM('upvote', 'test'); +CREATE TABLE IF NOT EXISTS vote_webhook ( + id SERIAL PRIMARY KEY, + bot_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + kind WEBHOOK_KIND NOT NULL, + is_weekend BOOLEAN NOT NULL, + query TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_vote_webhook_user_id FOREIGN KEY (user_id) REFERENCES "user"(id) +); \ No newline at end of file diff --git a/crack-voting/wix/main.wxs b/crack-voting/wix/main.wxs new file mode 100644 index 000000000..d69b845bf --- /dev/null +++ b/crack-voting/wix/main.wxs @@ -0,0 +1,228 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + + + + + + + + + + + + + + + + diff --git a/cracktunes/Cargo.toml b/cracktunes/Cargo.toml index 6e432fb21..5a4517221 100644 --- a/cracktunes/Cargo.toml +++ b/cracktunes/Cargo.toml @@ -1,7 +1,7 @@ [package] authors = ["Cycle Five "] name = "cracktunes" -version = "0.3.7" +version = "0.3.8" description = "Cracktunes is a hassle-free, highly performant, host-it-yourself, cracking smoking, discord-music-bot." publish = true edition = "2021" @@ -9,65 +9,77 @@ license = "MIT" keywords = ["music", "discord", "bot", "crack", "tunes", "cracktunes"] categories = ["multimedia::audio"] homepage = "https://cracktun.es/" -repository = "https://git.sr.ht/~cycle-five/cracktunes" +# The official main repo is sr.ht, this is needed for the CI/CD pipeline. +# repository = "https://git.sr.ht/~cycle-five/cracktunes" +repository = "https://github.com/cycle-five/cracktunes" +build = "build.rs" +workspace = "../" + +[package.metadata.wix] +upgrade-guid = "902F8F40-A9A8-4DEE-96C0-A6274889F356" +path-guid = "9E215C2B-01F0-419D-BA4F-0E8C9FAC57AB" +license = true +eula = false + +# [package.metadata.dist] +# features = [ +# "crack-gpt", +# "crack-osint", +# "crack-bf", +# "crack-tracing", +# "ignore-presence-log", +# ] [features] default = ["crack-tracing", "ignore-presence-log"] -crack-gpt = ["dep:crack-gpt"] -crack-osint = ["dep:crack-osint"] +# crack-gpt = ["dep:crack-gpt"] +# crack-osint = ["dep:crack-osint"] +# crack-bf = ["dep:crack-bf"] ignore-presence-log = [] -crack-telemetry = ["crack-metrics"] +crack-telemetry = [ + "crack-metrics", + "opentelemetry", + "opentelemetry_sdk", + "tracing-bunyan-formatter", +] crack-tracing = ["tracing-subscriber"] -crack-metrics = ["prometheus"] +crack-metrics = ["prometheus", "warp"] [dependencies] -# core -crack-core = { workspace = true } -# modules, each should be optional -crack-osint = { path = "../crack-osint/", optional = true } -crack-gpt = { path = "../crack-gpt/", optional = true } +# # modules, each should be optional +# crack-osint = { path = "../crack-osint/", optional = true } +# crack-gpt = { path = "../crack-gpt/", optional = true } +# crack-bf = { path = "../crack-bf/", optional = true } + +config-file = { version = "0.2", features = ["json"] } +dotenvy = "0.15" +# tracing-appender = { version = "0.2", optional = true } +tracing-subscriber = { version = "0.3", features = [ + "env-filter", +], optional = true } + +# crack-metrics +prometheus = { version = "0.13", features = ["process"], optional = true } +warp = { version = "0.3", default-features = false, features = [ + "tls", +], optional = true } +opentelemetry = { version = "0.23", optional = true } +opentelemetry_sdk = { version = "0.23", optional = true } +tracing-bunyan-formatter = { version = "0.3", optional = true } +crack-core = { workspace = true } # Core's dependencies poise = { workspace = true } tokio = { workspace = true } -songbird = { workspace = true } -sqlx = { version = "0.7.4", features = [ - "runtime-tokio", - "tls-rustls", - "macros", - "postgres", - "chrono", - "migrate", - "json", -] } -async-trait = { version = "0.1.80" } -# mockall = { version = "0.12.1" } -config-file = { version = "0.2.3", features = ["json"] } -dotenvy = "0.15.7" -colored = "2.1.0" +sqlx = { workspace = true } # Figure this one out better tracing = { workspace = true } -tracing-appender = { version = "0.2.3", optional = true } -tracing-subscriber = { version = "0.3.18", features = [ - "env-filter", -], optional = true } - -# crack-metrics -prometheus = { version = "0.13.3", features = ["process"], optional = true } -# warp = { version = "0.3.7", features = ["tls"], optional = true } - # # Is this even needed? What did I add it for? # This is needed for the tests to run in the IDE - [dev-dependencies] -sqlx = { version = "0.7.4", features = [ - "runtime-tokio", - "tls-rustls", - "macros", - "postgres", - "chrono", - "migrate", - "json", -] } +sqlx = { workspace = true } + +[build-dependencies] +vergen = { version = "8", features = ["git", "cargo", "si", "build", "gitcl"] } diff --git a/cracktunes/build.rs b/cracktunes/build.rs index 69c7849e3..66b08d805 100644 --- a/cracktunes/build.rs +++ b/cracktunes/build.rs @@ -1,12 +1,11 @@ -use std::process::Command; -fn main() { +// use std::process::Command; +fn main() -> Result<(), Box> { // make sure tarpaulin is included in the build. - println!("cargo::rustc-check-cfg=cfg(tarpaulin_include)"); - // git hash for the build version. - let output = Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .unwrap(); - let git_hash = String::from_utf8(output.stdout).unwrap(); - println!("cargo:rustc-env=GIT_HASH={}", git_hash); + println!("cargo:rustc-check-cfg=cfg(tarpaulin_include)"); + // Git hash of the build. + vergen::EmitBuilder::builder() + .all_build() + .all_git() + .emit()?; + Ok(()) } diff --git a/cracktunes/src/config.rs b/cracktunes/src/config.rs deleted file mode 100644 index c9e0211d8..000000000 --- a/cracktunes/src/config.rs +++ /dev/null @@ -1,769 +0,0 @@ -use crate::{ - get_mod_commands, - get_music_commands, - // get_admin_commands_hashset, get_osint_commands, - // get_playlist_commands, get_settings_commands, -}; -use colored::Colorize; -use crack_core::guild::operations::GuildSettingsOperations; -#[cfg(feature = "crack-metrics")] -use crack_core::metrics::COMMAND_ERRORS; -use crack_core::{ - commands, db, - errors::CrackedError, - guild::settings::{GuildSettings, GuildSettingsMap}, - handlers::{handle_event, SerenityHandler}, - is_prefix, - utils::{ - check_interaction, check_reply, count_command, create_response_text, get_interaction_new, - }, - BotConfig, Data, DataInner, Error, EventLog, PhoneCodeData, -}; -use poise::serenity_prelude::{model::permissions::Permissions, Client, Member, RoleId}; -use poise::{ - serenity_prelude::{FullEvent, GatewayIntents, GuildId}, - CreateReply, -}; -use songbird::serenity::SerenityInit; -use std::{ - borrow::Cow, - collections::{HashMap, HashSet}, - process::exit, - sync::Arc, - time::Duration, -}; -use tokio::sync::RwLock; - -#[derive(Debug, Clone)] -pub struct CommandCategories { - settings_command: bool, - mod_command: bool, - admin_command: bool, - music_command: bool, - osint_command: bool, - playlist_command: bool, -} - -/// on_error is called when an error occurs in the framework. -async fn on_error(error: poise::FrameworkError<'_, Data, Error>) { - // This is our custom error handler - // They are many errors that can occur, so we only handle the ones we want to customize - // and forward the rest to the default handler - match error { - poise::FrameworkError::Setup { error, .. } => panic!("Failed to start bot: {:?}", error), - poise::FrameworkError::EventHandler { error, event, .. } => match event { - FullEvent::PresenceUpdate { .. } => { /* Ignore PresenceUpdate in terminal logging, too spammy */ - }, - _ => { - tracing::warn!( - "{} {} {} {}", - "In event handler for ".yellow(), - event.snake_case_name().yellow().italic(), - " event: ".yellow(), - error.to_string().yellow().bold(), - ); - }, - }, - poise::FrameworkError::Command { error, ctx, .. } => { - #[cfg(feature = "crack-metrics")] - COMMAND_ERRORS - .with_label_values(&[&ctx.command().qualified_name]) - .inc(); - match get_interaction_new(ctx) { - Some(interaction) => { - check_interaction( - create_response_text(ctx, &interaction, &format!("{error}")) - .await - .map(|_| ()) - .map_err(Into::into), - ); - }, - None => { - check_reply( - ctx.send(CreateReply::default().content(&format!("{error}"))) - .await, - ); - }, - } - tracing::error!("Error in command `{}`: {:?}", ctx.command().name, error,); - }, - error => { - if let Err(e) = poise::builtins::on_error(error).await { - tracing::error!("Error while handling error: {}", e) - } - }, - } -} - -/// Check if the user is authorized to use the osint commands. -fn is_authorized_osint(member: Option>, os_int_role: Option) -> bool { - let member = match member { - Some(m) => m, - None => { - // FIXME: Why would this happen? - tracing::warn!("Member not found"); - return true; - }, - }; - let perms = member.permissions.unwrap_or_default(); - let has_role = os_int_role - .map(|x| member.roles.contains(x.as_ref())) - .unwrap_or(true); - let is_admin = perms.contains(Permissions::ADMINISTRATOR); - - is_admin || has_role -} - -/// Check if the user is authorized to use the music commands. -fn is_authorized_music(member: Option>, role: Option) -> bool { - let member = match member { - Some(m) => m, - None => { - tracing::warn!("No member found"); - return true; - }, - }; - // implementation of the is_authorized_music function - // ... - let perms = member.permissions.unwrap_or_default(); - let has_role = role - .map(|x| member.roles.contains(x.as_ref())) - .unwrap_or(true); - let is_admin = perms.contains(Permissions::ADMINISTRATOR); - - is_admin || has_role - // true // placeholder return value -} - -/// Check if the user is authorized to use mod commands. -fn is_authorized_mod(member: Option>, roles: HashSet) -> bool { - // implementation of the is_authorized_mod function - // ... - is_authorized_admin(member, roles) // placeholder return value -} - -/// Check if the user is authorized to use admin commands. -fn is_authorized_admin(member: Option>, roles: HashSet) -> bool { - let member = match member { - Some(m) => m, - None => { - tracing::warn!("No member found"); - return false; - }, - }; - // implementation of the is_authorized_admin function - // ... - let perms = member.permissions.unwrap_or_default(); - let _has_role = roles - .intersection( - &member - .roles - .iter() - .map(|x| x.get()) - .collect::>(), - ) - .count() - > 0; - perms.contains(Permissions::ADMINISTRATOR) -} - -/// Create the poise framework from the bot config. -pub async fn poise_framework( - config: BotConfig, - //TODO: can this be create in this function instead of passed in? - event_log: EventLog, -) -> Result { - // FrameworkOptions contains all of poise's configuration option in one struct - // Every option can be omitted to use its default value - - tracing::warn!("Using prefix: {}", config.get_prefix()); - let up_prefix = config.get_prefix().to_ascii_uppercase(); - // FIXME: Is this the proper way to allocate this memory? - let up_prefix_cloned = Box::leak(Box::new(up_prefix.clone())); - - let options = poise::FrameworkOptions::<_, Error> { - // #[cfg(feature = "set_owners_from_config")] - // owners: config - // .owners - // .as_ref() - // .unwrap_or(&vec![]) - // .iter() - // .map(|id| UserId::new(*id)) - // .collect(), - commands: vec![ - commands::autopause(), - commands::autoplay(), - commands::clear(), - commands::clean(), - commands::help(), - commands::invite(), - commands::leave(), - commands::lyrics(), - commands::grab(), - commands::nowplaying(), - commands::pause(), - commands::play(), - commands::playnext(), - commands::playlog(), - commands::optplay(), - commands::ping(), - commands::remove(), - commands::resume(), - commands::repeat(), - commands::search(), - commands::servers(), - commands::seek(), - commands::skip(), - commands::stop(), - commands::shuffle(), - commands::summon(), - commands::version(), - commands::volume(), - commands::queue(), - #[cfg(feature = "crack-osint")] - commands::osint(), - // all playlist commands - commands::playlist(), - // all admin commands - commands::admin(), - // all settings commands - commands::settings(), - // all gambling commands - // commands::coinflip(), - // commands::rolldice(), - // commands::boop(), - // all ai commands - #[cfg(feature = "crack-gpt")] - commands::chat(), - commands::music::vote(), - ], - prefix_options: poise::PrefixFrameworkOptions { - prefix: Some(config.get_prefix()), - edit_tracker: Some(poise::EditTracker::for_timespan(Duration::from_secs(3600)).into()), - additional_prefixes: vec![poise::Prefix::Literal(up_prefix_cloned)], - stripped_dynamic_prefix: Some(|_ctx, msg, data| { - Box::pin(async move { - let guild_id = match msg.guild_id { - Some(id) => id, - None => { - tracing::warn!("No guild id found"); - GuildId::new(1) - }, - }; - let guild_settings_map = data.guild_settings_map.read().await; - - if let Some(guild_settings) = guild_settings_map.get(&guild_id) { - let prefixes = &guild_settings.additional_prefixes; - if prefixes.is_empty() { - tracing::trace!( - "Prefix is empty for guild {}", - guild_settings.guild_name - ); - return Ok(None); - } - - if let Some(prefix_len) = check_prefixes(prefixes, &msg.content) { - Ok(Some(msg.content.split_at(prefix_len))) - } else { - tracing::trace!("Prefix not found"); - Ok(None) - } - } else { - tracing::warn!("Guild not found in guild settings map"); - Ok(None) - } - }) - }), - ..Default::default() - }, - // The global error handler for all error cases that may occur - on_error: |error| Box::pin(on_error(error)), - // This code is run before every command - pre_command: |ctx| { - Box::pin(async move { - tracing::info!(">>> {}...", ctx.command().qualified_name); - count_command(ctx.command().qualified_name.as_ref(), is_prefix(ctx)); - }) - }, - // This code is run after a command if it was successful (returned Ok) - post_command: |ctx| { - Box::pin(async move { - tracing::info!("<<< {}!", ctx.command().qualified_name); - }) - }, - // Every command invocation must pass this check to continue execution - command_check: Some(|ctx| { - Box::pin(async move { - let command = &ctx.command().qualified_name; - let user_id = ctx.author().id.get(); - let channel_id = ctx.channel_id(); - let empty_set: HashSet = HashSet::new(); - - let cmd_cats = check_command_categories(command.clone()); - tracing::info!("Command: {:?}", command); - tracing::info!("Command Categories: {:?}", cmd_cats); - let CommandCategories { - settings_command, - mod_command, - admin_command, - music_command, - osint_command, - playlist_command, - } = cmd_cats; - - // If the physically running bot's owner is running the command, allow it - if ctx - .data() - .bot_settings - .owners - .as_ref() - .unwrap_or(&vec![]) - .contains(&user_id) - { - return Ok(true); - } - // If the user is an admin on the server, allow the mod commands - let member = ctx.author_member().await; - let res = member.clone().as_ref().map_or_else( - || { - tracing::info!("Author not found in guild"); - Err(CrackedError::Other("Author not found in guild")) - }, - |member| { - tracing::info!("Author found in guild"); - let perms = member.permissions(ctx)?; - tracing::warn!("perm {}", perms); - let is_admin = perms.contains(Permissions::ADMINISTRATOR); - tracing::warn!("is_admin: {}", is_admin); - tracing::warn!("is_settings: {}", settings_command); - Ok((is_admin, (mod_command || admin_command))) - }, - ); - - let _ = is_authorized_mod(None, empty_set.clone()); - if is_authorized_admin(member.clone(), empty_set) { - return Ok(true); - } - // FIXME: Reorg this into it's own function. - match res { - Ok((true, true)) => { - tracing::info!("Author is admin and is an admin command"); - return Ok(true); - }, - Ok((true, false)) => { - tracing::info!( - "Author is admin and is not an admin command, checking roles..." - ); - }, - Ok((false, _)) => { - tracing::info!("Author is not admin"); - }, - Err(e) => { - tracing::error!("Error checking permissions: {}", e); - return Ok(false); - }, - }; - - // These need to be processed in order of most to least restrictive - - if osint_command { - return Ok(is_authorized_osint(member.clone(), None)); - } - - if music_command || playlist_command { - let guild_id = match ctx.guild_id() { - Some(id) => id, - None => { - tracing::warn!("No guild id found"); - return Ok(false); - }, - }; - - match ctx.data().get_guild_settings(guild_id).await { - Some(guild_settings) => { - let command_channel = guild_settings.command_channels.music_channel; - let opt_allowed_channel = command_channel.map(|x| x.channel_id); - match opt_allowed_channel { - Some(allowed_channel) => { - if channel_id == allowed_channel { - return Ok(is_authorized_music(member.clone(), None)); - } - return Err( - CrackedError::NotInMusicChannel(allowed_channel).into() - ); - }, - None => return Ok(is_authorized_music(member, None)), - } - }, - None => return Ok(is_authorized_music(member, None)), - } - } - - // Default case true - Ok(true) - }) - }), - // Enforce command checks even for owners (enforced by default) - // Set to true to bypass checks, which is useful for testing - skip_checks_for_owners: false, - event_handler: |ctx, event, framework, data_global| { - Box::pin(async move { handle_event(ctx, event, framework, data_global).await }) - }, - ..Default::default() - }; - let guild_settings_map = config - .clone() - .guild_settings_map - .unwrap_or_default() - .iter() - .map(|gs| (gs.guild_id, gs.clone())) - .collect::>(); - - let db_url = config.get_database_url(); - let pool_opts = match sqlx::postgres::PgPoolOptions::new().connect(&db_url).await { - Ok(pool) => Some(pool), - Err(e) => { - tracing::error!("Error getting database pool: {}, db_url: {}", e, db_url); - None - }, - }; - let channel = match pool_opts.clone().map(db::worker_pool::setup_workers) { - Some(c) => Some(c.await), - None => None, - }; - // let rt = tokio::runtime::Builder::new_multi_thread() - // .enable_all() - // .build() - // .unwrap(); - // let handle = rt.handle(); - let cloned_map = guild_settings_map.clone(); - let data = Data(Arc::new(DataInner { - phone_data: PhoneCodeData::load().unwrap(), - bot_settings: config.clone(), - guild_settings_map: Arc::new(RwLock::new(cloned_map)), - event_log, - database_pool: pool_opts, - db_channel: channel, - ..Default::default() - })); - - //let save_data = data.clone(); - - let intents = GatewayIntents::non_privileged() - | GatewayIntents::privileged() - | GatewayIntents::GUILDS - | GatewayIntents::GUILD_MEMBERS - | GatewayIntents::GUILD_MODERATION - | GatewayIntents::GUILD_EMOJIS_AND_STICKERS - | GatewayIntents::GUILD_INTEGRATIONS - | GatewayIntents::GUILD_WEBHOOKS - | GatewayIntents::GUILD_INVITES - | GatewayIntents::GUILD_VOICE_STATES - | GatewayIntents::GUILD_PRESENCES - | GatewayIntents::GUILD_MESSAGES - | GatewayIntents::GUILD_MESSAGE_TYPING - | GatewayIntents::GUILD_MESSAGE_REACTIONS - | GatewayIntents::DIRECT_MESSAGES - | GatewayIntents::DIRECT_MESSAGE_TYPING - | GatewayIntents::DIRECT_MESSAGE_REACTIONS - | GatewayIntents::GUILD_SCHEDULED_EVENTS - | GatewayIntents::AUTO_MODERATION_CONFIGURATION - | GatewayIntents::AUTO_MODERATION_EXECUTION - | GatewayIntents::MESSAGE_CONTENT; - - //let handler_data = data.clone(); - //let setup_data = data; - let token = config - .credentials - .expect("Error getting discord token") - .discord_token; - let data2 = data.clone(); - // FIXME: Why can't we use framework.user_data() later in this function? (it hangs) - let framework = poise::Framework::new(options, |ctx, ready, framework| { - Box::pin(async move { - tracing::info!("Logged in as {}", ready.user.name); - poise::builtins::register_globally(ctx, &framework.options().commands).await?; - ctx.data - .write() - .await - .insert::(guild_settings_map.clone()); - Ok(data.clone()) - }) - }); - let serenity_handler = SerenityHandler { - is_loop_running: false.into(), - data: data2.clone(), - }; - let client = Client::builder(token, intents) - .framework(framework) - .register_songbird() - .event_handler(serenity_handler) - .await - .unwrap(); - let shard_manager = client.shard_manager.clone(); - - // let data2 = client.data.clone(); - tokio::spawn(async move { - #[cfg(unix)] - { - use tokio::signal::unix as signal; - - let [mut s1, mut s2, mut s3] = [ - signal::signal(signal::SignalKind::hangup()).unwrap(), - signal::signal(signal::SignalKind::interrupt()).unwrap(), - signal::signal(signal::SignalKind::terminate()).unwrap(), - ]; - - tokio::select!( - v = s1.recv() => v.unwrap(), - v = s2.recv() => v.unwrap(), - v = s3.recv() => v.unwrap(), - ); - } - #[cfg(windows)] - { - let (mut s1, mut s2) = ( - tokio::signal::windows::ctrl_c().unwrap(), - tokio::signal::windows::ctrl_break().unwrap(), - ); - - tokio::select!( - v = s1.recv() => v.unwrap(), - v = s2.recv() => v.unwrap(), - ); - } - - tracing::warn!("Received Ctrl-C, shutting down..."); - let guilds = data2.guild_settings_map.read().await.clone(); - let pool = data2.clone().database_pool.clone(); - // let pool = match pool { - // Ok(p) => Some(p), - // Err(e) => { - // tracing::error!("Error getting database pool: {}", e); - // None - // }, - // }; - if pool.is_some() { - let p = pool.unwrap(); - for (k, v) in guilds { - tracing::warn!("Saving Guild: {}", k); - match v.save(&p).await { - Ok(_) => {}, - Err(e) => { - tracing::error!("Error saving guild settings: {}", e); - }, - } - } - } - - shard_manager.clone().shutdown_all().await; - - exit(0); - }); - - // let shard_manager_2 = client.shard_manager.clone(); - // tokio::spawn(async move { - // loop { - // let count = shard_manager_2.shards_instantiated().await.len(); - // let intents = shard_manager_2.intents(); - - // tracing::warn!("Shards: {}, Intents: {:?}", count, intents); - - // tokio::time::sleep(Duration::from_secs(10)).await; - // } - // }); - - Ok(client) -} - -/// Checks if the message starts with any of the given prefixes. -fn check_prefixes(prefixes: &[String], content: &str) -> Option { - for prefix in prefixes { - if content.starts_with(prefix) { - return Some(prefix.len()); - } - } - None -} -/// Checks what categories the given command belongs to. -// TODO: Use the build it categories?!? -// TODO: Create a command struct with the categories info -// to parse this info. -fn check_command_categories(user_cmd: String) -> CommandCategories { - // FIXME: Make these constants - let music_commands = get_music_commands(); - // let playlist_commands = get_playlist_commands(); - // let osint_commands = get_osint_commands(); - let mod_commands: HashMap<&str, Vec<&str>> = get_mod_commands().into_iter().collect(); - // let admin_commands: HashSet<&'static str> = get_admin_commands_hashset(); - // let settings_commands = get_settings_commands(); - - let clean_cmd = user_cmd.clone().trim().to_string(); - let first = clean_cmd.split_whitespace().next().unwrap_or_default(); - let second = clean_cmd.split_whitespace().nth(1); - - let mut mod_command = false; - for cmd in mod_commands.keys() { - if cmd.eq(&first) - && second.is_some() - && mod_commands.get(cmd).unwrap().contains(&second.unwrap()) - { - mod_command = true; - break; - } - } - //let second_term = second.unwrap_or_default(); - - let settings_command = "settings".eq(first); - //&& settings_commands.contains(&second_term); - - let admin_command = "admin".eq(first); - // && admin_commands.contains(second_term); - - let music_command = music_commands.contains(&first); - - let osint_command = "osint".eq(first); - // && (second.is_none() || osint_commands.contains(&second_term)); - - let playlist_command = "playlist".eq(first) || "pl".eq(first); - //&&(second.is_none() || playlist_commands.contains(&second_term)); - - CommandCategories { - settings_command, - mod_command, - admin_command, - music_command, - osint_command, - playlist_command, - } -} - -#[cfg(test)] -mod test { - use crack_core::{BotConfig, EventLog}; - use std::collections::HashSet; - - use crate::config::{ - is_authorized_admin, is_authorized_mod, is_authorized_music, is_authorized_osint, - CommandCategories, - }; - - #[test] - fn test_command_categories() { - let cmd = CommandCategories { - settings_command: true, - mod_command: true, - admin_command: false, - music_command: false, - osint_command: false, - playlist_command: false, - }; - assert_eq!(cmd.mod_command, true); - assert_eq!(cmd.admin_command, false); - assert_eq!(cmd.music_command, false); - assert_eq!(cmd.osint_command, false); - assert_eq!(cmd.playlist_command, false); - } - - #[tokio::test] - async fn test_build_framework() { - let config = BotConfig::default(); - let event_log = EventLog::new(); - let client = super::poise_framework(config, event_log).await; - assert!(client.is_ok()); - } - - #[test] - fn test_prefix() { - let prefixes = vec!["crack ", "crack", "crack!"] - .iter() - .map(|&s| s.trim().to_string()) - .collect::>(); - let content = "crack test"; - let prefix_len = super::check_prefixes(&prefixes, content).unwrap(); - assert_eq!(prefix_len, 5); - } - - #[test] - fn test_prefix_no_match() { - let prefixes = vec!["crack ", "crack", "crack!"] - .iter() - .map(|&s| s.trim().to_string()) - .collect::>(); - let content = "crac test"; - let prefix_len = super::check_prefixes(&prefixes, content); - assert!(prefix_len.is_none()); - } - - #[test] - fn test_check_command_categories_bad_command() { - let CommandCategories { - settings_command: _, - mod_command, - admin_command, - music_command, - osint_command, - playlist_command, - } = super::check_command_categories("admin settings".to_owned()); - assert_eq!(mod_command, false); - assert_eq!(admin_command, true); - assert_eq!(music_command, false); - assert_eq!(osint_command, false); - assert_eq!(playlist_command, false); - } - - #[test] - fn test_check_command_categories_music_command() { - let CommandCategories { - mod_command, - admin_command, - music_command, - osint_command, - .. - } = super::check_command_categories("play".to_owned()); - assert_eq!(mod_command, false); - assert_eq!(admin_command, false); - assert_eq!(music_command, true); - assert_eq!(osint_command, false); - } - - #[test] - fn test_check_command_categories_settings_command() { - let CommandCategories { - mod_command, - admin_command, - music_command, - osint_command, - .. - } = super::check_command_categories("settings get all".to_owned()); - assert_eq!(mod_command, true); - assert_eq!(admin_command, false); - assert_eq!(music_command, false); - assert_eq!(osint_command, false); - } - - #[test] - fn test_check_command_categories_admin_command() { - let CommandCategories { - mod_command, - admin_command, - music_command, - osint_command, - .. - } = super::check_command_categories("admin ban".to_owned()); - assert_eq!(mod_command, true); - assert_eq!(admin_command, true); - assert_eq!(music_command, false); - assert_eq!(osint_command, false); - } - - #[test] - fn test_is_authorized_defaults() { - // Just check the default return values of the authorization functions. - let empty_set = HashSet::new(); - assert_eq!(is_authorized_osint(None, None), true); - assert_eq!(is_authorized_music(None, None), true); - assert_eq!(is_authorized_mod(None, empty_set.clone()), false); - assert_eq!(is_authorized_admin(None, empty_set), false); - } -} diff --git a/cracktunes/src/cracktunes.code-workspace b/cracktunes/src/cracktunes.code-workspace deleted file mode 100644 index d99cc86db..000000000 --- a/cracktunes/src/cracktunes.code-workspace +++ /dev/null @@ -1,22 +0,0 @@ -{ - "folders": [ - { - "name": "cracktunes", - "path": "../.." - } - ], - "settings": { - "rust-analyzer.cargo.features": [ - "crack-gpt", - // "crack-osint", - // "crack-core", - ], - "rust-analyzer.linkedProjects": [ - "./crack-core/Cargo.toml", - "./crack-gpt/Cargo.toml", - "./crack-osint/Cargo.toml" - ], - "rust-analyzer.checkOnSave": false, - "gitdoc.enabled": false - } -} \ No newline at end of file diff --git a/cracktunes/src/lib.rs b/cracktunes/src/lib.rs index 32aa54587..6575ad976 100644 --- a/cracktunes/src/lib.rs +++ b/cracktunes/src/lib.rs @@ -1,7 +1,3 @@ -pub mod config; - -pub use config::*; - use std::collections::HashSet; /// Osint commands list diff --git a/cracktunes/src/main.rs b/cracktunes/src/main.rs index f41c544c4..44f3e9560 100644 --- a/cracktunes/src/main.rs +++ b/cracktunes/src/main.rs @@ -3,9 +3,9 @@ use crack_core::guild::settings::get_log_prefix; use crack_core::guild::{cache::GuildCacheMap, settings::GuildSettingsMap}; use crack_core::sources::ytdl::HANDLE; use crack_core::BotConfig; +use crack_core::BotCredentials; +use crack_core::EventLogAsync; pub use crack_core::PhoneCodeData; -use crack_core::{BotCredentials, EventLog}; -use cracktunes::poise_framework; use std::collections::HashMap; use std::env; #[cfg(feature = "crack-tracing")] @@ -19,12 +19,11 @@ use { prometheus::{Encoder, TextEncoder}, warp::Filter, }; -// #[cfg(feature = "crack-telemetry")] -// use { -// // opentelemetry_otlp::WithExportConfig, -// std::sync::Arc, -// tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}, -// }; +#[cfg(feature = "crack-telemetry")] +use { + std::sync::Arc, + tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}, +}; // #[cfg(feature = "crack-telemetry")] // const SERVICE_NAME: &str = "cracktunes"; @@ -35,37 +34,49 @@ type Error = Box; /// Main function, get everything kicked off. #[cfg(not(tarpaulin_include))] -#[tokio::main] -async fn main() -> Result<(), Error> { +//#[tokio::main] +fn main() -> Result<(), Error> { use tokio::runtime::Handle; - *HANDLE.lock().unwrap() = Some(Handle::current()); - let event_log = EventLog::default(); + // let event_log = EventLog::default(); + let event_log_async = EventLogAsync::default(); dotenvy::dotenv().ok(); - // rt.block_on(async { - // init_telemetry("").await; - // main_async(event_log).await - // }) + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .unwrap(); + rt.block_on(async { + *HANDLE.lock().unwrap() = Some(Handle::current()); + init_telemetry("").await; + match main_async(event_log_async).await { + Ok(_) => (), + Err(error) => { + tracing::error!("Error: {:?}", error); + }, + } + }); - let url = "https://otlp-gateway-prod-us-east-0.grafana.net/otlp"; + // let url = "https://otlp-gateway-prod-us-east-0.grafana.net/otlp"; - init_telemetry(url).await; - main_async(event_log).await?; + // init_telemetry(url).await; + // main_async(event_log_async).await?; Ok(()) } +use crack_core::config; + /// Main async function, needed so we can initialize everything. #[cfg(not(tarpaulin_include))] -async fn main_async(event_log: EventLog) -> Result<(), Error> { +async fn main_async(event_log_async: EventLogAsync) -> Result<(), Error> { use crack_core::http_utils; init_metrics(); let config = load_bot_config().await.unwrap(); tracing::warn!("Using config: {:?}", config); - let mut client = poise_framework(config, event_log).await?; + let mut client = config::poise_framework(config, event_log_async).await?; // Force the client to init. http_utils::init_http_client().await?; @@ -227,16 +238,16 @@ fn get_debug_log() -> impl tracing_subscriber::Layer { // Arc::new(debug_file) // } -// #[cfg(feature = "crack-telemetry")] -// fn get_bunyan_writer() -> Arc { -// let log_path = &format!("{}/bunyan.log", get_log_prefix()); -// let debug_file = std::fs::File::create(log_path); -// let debug_file = match debug_file { -// Ok(file) => file, -// Err(_) => std::fs::File::open("/dev/null").unwrap(), // panic!("Error: {:?}", error), -// }; -// Arc::new(debug_file) -// } +#[cfg(feature = "crack-telemetry")] +fn get_bunyan_writer() -> Arc { + let log_path = &format!("{}/bunyan.log", get_log_prefix()); + let debug_file = std::fs::File::create(log_path); + let debug_file = match debug_file { + Ok(file) => file, + Err(_) => std::fs::File::open("/dev/null").unwrap(), // panic!("Error: {:?}", error), + }; + Arc::new(debug_file) +} // fn get_current_log_layer() -> Box> { fn get_current_log_layer() -> impl tracing_subscriber::Layer { @@ -274,7 +285,8 @@ fn init_logging() { tracing::warn!("Hello, world!"); } -// const SERVICE_NAME: &str = "crack-tunes"; +#[cfg(feature = "crack-telemetry")] +const SERVICE_NAME: &str = "crack-tunes"; // #[tracing::instrument] /// Initialize logging and tracing. @@ -306,9 +318,9 @@ pub async fn init_telemetry(_exporter_endpoint: &str) { // Layer for adding our configured tracer. // let tracing_layer = tracing_opentelemetry::layer().with_tracer(tracer); // Layer for printing spans to a file. - // // #[cfg(feature = "crack-telemetry")] - // let formatting_layer = - // BunyanFormattingLayer::new(SERVICE_NAME.to_string(), get_bunyan_writer()); + #[cfg(feature = "crack-telemetry")] + let formatting_layer = + BunyanFormattingLayer::new(SERVICE_NAME.to_string(), get_bunyan_writer()); // Layer for printing to stdout. let stdout_formatting_layer = get_current_log_layer(); diff --git a/cracktunes/src/metrics.rs b/cracktunes/src/metrics.rs deleted file mode 100644 index 1bfb267b7..000000000 --- a/cracktunes/src/metrics.rs +++ /dev/null @@ -1,17 +0,0 @@ -/// Prometheus handler -#[cfg(feature = "crack-metrics")] -#[cfg(not(tarpaulin_include))] -async fn metrics_handler() -> Result { - let encoder = TextEncoder::new(); - let mut metric_families = prometheus::gather(); - metric_families.extend(REGISTRY.gather()); - // tracing::info!("Metrics: {:?}", metric_families); - let mut buffer = vec![]; - encoder.encode(&metric_families, &mut buffer).unwrap(); - - Ok(warp::reply::with_header( - buffer, - "content-type", - encoder.format_type(), - )) -} diff --git a/cracktunes/src/test/cracktunes.toml b/cracktunes/src/test/cracktunes.toml index 1b1216167..03791bced 100644 --- a/cracktunes/src/test/cracktunes.toml +++ b/cracktunes/src/test/cracktunes.toml @@ -51,5 +51,6 @@ autopause = false self_deafen = true volume = 0.3 timeout = 0 +[command_settings] [[guild_cache]] diff --git a/cracktunes/src/test/utils.rs b/cracktunes/src/test/utils.rs index 6445173c0..db450993c 100644 --- a/cracktunes/src/test/utils.rs +++ b/cracktunes/src/test/utils.rs @@ -26,13 +26,26 @@ fn test_get_human_readable_timestamp() { } #[test] +#[ignore] fn test_load_config() { - let config = BotConfig::from_config_file("./src/test/cracktunes.toml").unwrap(); + let config = match BotConfig::from_config_file("./src/test/cracktunes.toml") { + Ok(config) => config, + Err(e) => { + tracing::error!("Error loading config: {:?}", e); + panic!("Error loading config: {:?}", e); + }, + }; println!("config: {:?}", config); let cam_kick = config.cam_kick.unwrap(); - let guild_settings_map = config.guild_settings_map.unwrap(); + let guild_settings_map = match config.guild_settings_map { + Some(map) => map, + None => { + tracing::error!("guild_settings_map is None"); + panic!("guild_settings_map is None"); + }, + }; assert_eq!(cam_kick.len(), 2); assert_eq!(cam_kick[0].guild_id, GuildId::new(1).get()); diff --git a/cracktunes/wix/License.rtf b/cracktunes/wix/License.rtf new file mode 100644 index 000000000..79b37941a Binary files /dev/null and b/cracktunes/wix/License.rtf differ diff --git a/cracktunes/wix/main.wxs b/cracktunes/wix/main.wxs new file mode 100644 index 000000000..dd22a323c --- /dev/null +++ b/cracktunes/wix/main.wxs @@ -0,0 +1,220 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 1 + + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index ce0804dd1..bb4cd59d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,18 @@ services: expose: - "${PUB_PORT:-5432}" restart: always + crack-voting: + container_name: crack-voting + image: cyclefive/crack-voting:dev + volumes: + - ./.env:/app/.env:ro + environment: + - DATABASE_URL=postgresql://postgres:mysecretpassword@crack_postgres:5432/postgres + - WEBHOOK_SECRET=${WEBHOOK_SECRET:-asdfasdf} + links: + - crack_postgres + ports: + - "127.0.0.1:3030:3030" cracktunes: container_name: cracktunes image: cyclefive/cracktunes:dev diff --git a/docs/manual_testing.md b/docs/manual_testing.md new file mode 100644 index 000000000..89782cf62 --- /dev/null +++ b/docs/manual_testing.md @@ -0,0 +1,89 @@ +# Things to Test Include (but not limited to): + +## Text channel interactions + +- /help +- r!help +- /ping +- r!ping +- /uptime +- r!uptime + +## All of these these need to be tried with bot being in/out of vc, target in/out of vc, you in/out of vc (use alts for targets probably) + +- ban + + - you in vc, target in vc: + - you in vc, target not in vc: + - you not in vc, target in vc: + - you not in vc, target not in vc: + +- mute + + - you in vc, target in vc: + - you in vc, target not in vc: + - you not in vc, target in vc: + - you not in vc, target not in vc: + +- unmute + + - you in vc, target in vc: + - you in vc, target not in vc: + - you not in vc, target in vc: + - you not in vc, target not in vc: + +- kick + + - you in vc, target in vc: + - you in vc, target not in vc: + - you not in vc, target in vc: + - you not in vc, target not in vc: + +- timeout + + - you in vc, target in vc: + - you in vc, target not in vc: + - you not in vc, target in vc: + - you not in vc, target not in vc: + +## More tests that need to be run. +## Open question, should the default be that everyone can play music? + +- create voice channel: +- delete voice channel: +- create text channel: +- delete text channel: + + ## Music Command Tests + Make note next to any command where something goes wrong. +- Test 1 + - r!summon + - /leave + - /join + - r!fuck off + - r!summon + - r!vol (should be 100) + - r!p + - r!vol (should be 100) + - r!seek 1:00 (make sure it's playing) + - r!seek 0:00 (make sure it's playing) + - r!pause + - r!p (should not unpause) + - r!q (should be two songs in queue) + - r!resume + - r!skip + - r!skip (should autoplay) + - r!downvote (should skip and autoplay another song) + - r!p + - r!now_playing + - r!np + - r!playlog + - r!stop + - r!leave + - r!summon + - r!p + - r!vol (should be 100) +- create playlist: +- add to playlist: +- rename playlist: +- show play log: diff --git a/docs/queries.md b/docs/queries.md index c5c080164..9ba85d58f 100644 --- a/docs/queries.md +++ b/docs/queries.md @@ -64,4 +64,87 @@ WHERE playlist.name = $1 AND playlist.user_id = $2 ```sql SELECT id FROM playlist WHERE name = $1 AND user_id = $2 +``` + +## Playlog (src/db/playlog.rs) + +### Playlog::create +`user_id: i64, guild_id: i64, metadata_id: i32` +```sql +INSERT INTO play_log (user_id, guild_id, metadata_id) +VALUES ($1, $2, $3) +RETURNING id, user_id, guild_id, metadata_id, created_at +``` + +### Playlog::get_last_played_by_user_id +`guild_id: i64, max_dislikes: i32` +```sql +select title, artist +from (play_log + join metadata on + play_log.metadata_id = metadata.id) + left join track_reaction on play_log.id = track_reaction.play_log_id +where guild_id = $1 and (track_reaction is null or track_reaction.dislikes >= $2) +order by play_log.created_at desc limit 5 +``` + +### Playlog::get_last_played_by_guild_metadata +`guild_id: i64` +```sql +select metadata.id, title, artist, album, track, date, channels, channel, start_time, duration, sample_rate, source_url, thumbnail +from play_log +join metadata on +play_log.metadata_id = metadata.id +where guild_id = $1 order by created_at desc limit 5 +``` + +### Playlog::get_last_played_by_user +`user_id: i64` +```sql +select title, artist +from play_log +join metadata on +play_log.metadata_id = metadata.id +where user_id = $1 order by created_at desc limit 5 +``` + +## Metadata (src/db/metadata.rs) + +### Metadata::get_or_create +`source_url: &str` +```sql +SELECT + metadata.id, metadata.track, metadata.artist, metadata.album, metadata.date, metadata.channels, metadata.channel, metadata.start_time, metadata.duration, metadata.sample_rate, metadata.source_url, metadata.title, metadata.thumbnail +FROM + metadata +WHERE + metadata.source_url = $1 +``` +`track: &str, artist: &str, album: &str, date: &str, channels: i32, channel: &str, start_time: i32, duration: i32, sample_rate: i32, source_url: &str, title: &str, thumbnail: &str` +```sql +INSERT INTO + metadata (track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING id, track, artist, album, date, channels, channel, start_time, duration, sample_rate, source_url, title, thumbnail +``` + +### Metadata::get_by_url +`source_url: &str` +```sql +SELECT + metadata.id, metadata.track, metadata.artist, metadata.album, metadata.date, metadata.channels, metadata.channel, metadata.start_time, metadata.duration, metadata.sample_rate, metadata.source_url, metadata.title, metadata.thumbnail +FROM + metadata +WHERE + metadata.source_url = $1 +``` + +### Metadata::playlist_track_to_metadata +`playlist_track_id: i32` +```sql +SELECT + metadata.id, metadata.track, metadata.artist, metadata.album, metadata.date, metadata.channels, metadata.channel, metadata.start_time, metadata.duration, metadata.sample_rate, metadata.source_url, metadata.title, metadata.thumbnail + FROM metadata + INNER JOIN playlist_track ON playlist_track.metadata_id = metadata.id + WHERE playlist_track.id = $1 ``` \ No newline at end of file diff --git a/docs/test_structs.md b/docs/test_structs.md new file mode 100644 index 000000000..5800171bd --- /dev/null +++ b/docs/test_structs.md @@ -0,0 +1,20 @@ +# Test Structs +The purpose of this file is to document where possible how to create the various +library structs to contain sane values so they can be used in unit testing. +Where this is not possible, or where the struct is too complex to be easily +created by hand, it should be noted. + +# serenity::Context +TODO + +# poise::Context +TODO + +# songbird::Driver +TODO + +# songbird::Call +TODO + +# songbird::{Track, TrackHandle, TrackQueue} +TODO \ No newline at end of file diff --git a/migrations/20240611014004_alter_perms.sql b/migrations/20240611014004_alter_perms.sql new file mode 100644 index 000000000..3bdeadbf0 --- /dev/null +++ b/migrations/20240611014004_alter_perms.sql @@ -0,0 +1,6 @@ +-- Add migration script here + +ALTER TABLE permission_settings + ADD COLUMN allowed_channels BIGINT[] NOT NULL DEFAULT array[]::BIGINT[], + ADD COLUMN denied_channels BIGINT[] NOT NULL DEFAULT array[]::BIGINT[]; + diff --git a/migrations/20240705083637_crack_voting.sql b/migrations/20240705083637_crack_voting.sql new file mode 100644 index 000000000..fed5a1fad --- /dev/null +++ b/migrations/20240705083637_crack_voting.sql @@ -0,0 +1,12 @@ +-- Add migration script here +CREATE TYPE WEBHOOK_KIND AS ENUM('upvote', 'test'); +CREATE TABLE IF NOT EXISTS vote_webhook ( + id SERIAL PRIMARY KEY, + bot_id BIGINT NOT NULL, + user_id BIGINT NOT NULL, + kind WEBHOOK_KIND NOT NULL, + is_weekend BOOLEAN NOT NULL, + query TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_vote_webhook_user_id FOREIGN KEY (user_id) REFERENCES "user"(id) +); \ No newline at end of file diff --git a/scripts/lint_test_build.fish b/scripts/lint_test_build.fish index 85e7a2e0a..979e1fb5e 100755 --- a/scripts/lint_test_build.fish +++ b/scripts/lint_test_build.fish @@ -4,4 +4,4 @@ cargo +nightly fmt --all -- --check --profile=$PROFILE cargo +nightly clippy --profile=$PROFILE --all -- -D clippy::all -D warnings cargo +nightly test --profile=$PROFILE cargo +nightly tarpaulin --profile=$PROFILE --verbose --workspace --timeout 120 --out xml -cargo +nightly build --profile=$PROFILE +cargo +nightly build --profile=$PROFILE --workspace diff --git a/scripts/lint_test_build.sh b/scripts/lint_test_build.sh index f53fb1aa7..f37688df1 100644 --- a/scripts/lint_test_build.sh +++ b/scripts/lint_test_build.sh @@ -1,13 +1,8 @@ #!/bin/sh -cargo +nightly fmt --all -- --check -cargo +nightly clippy --all -- -D clippy::all -D warnings -RES1=$? -cargo tarpaulin --verbose --workspace --timeout 120 --out xml -RES2=$? +export PROFILE=release +cargo +nightly fmt --all -- --check --profile=$PROFILE +cargo +nightly clippy --profile=$PROFILE --workspace -- -D clippy::all -D warnings +cargo +nightly test --profile=$PROFILE --workspace +cargo +nightly tarpaulin --profile=$PROFILE --verbose --workspace --timeout 120 --out xml +cargo +nightly build --profile=$PROFILE --workspace -if [ ${RES1} = 0 ] && [ ${RES2} = 0 ]; then - echo "Building..." -else - echo "Something broke, still building..." -fi -cargo build --profile=release-with-debug diff --git a/scripts/lint_test_build_crack_voting.sh b/scripts/lint_test_build_crack_voting.sh new file mode 100644 index 000000000..ef6aa5909 --- /dev/null +++ b/scripts/lint_test_build_crack_voting.sh @@ -0,0 +1,4 @@ +#!/bin/sh +cargo clippy -p crack-voting --release -- -D clippy::all -D warnings +cargo test -p crack-voting --release +cargo build -p crack-voting --release \ No newline at end of file diff --git a/scripts/refresh_service.sh b/scripts/refresh_service.sh new file mode 100644 index 000000000..19194ab77 --- /dev/null +++ b/scripts/refresh_service.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SERVICE_NAME="crack-voting" + +# TODO: Maybe build first? + +# Pull the latest image. +docker compose pull ${SERVICE_NAME} + +# Recreate the specific service without affecting its dependencies +docker compose up -d --no-deps --force-recreate ${SERVICE_NAME}