diff --git a/.cargo/config.toml b/.cargo/config.toml index 913f2d5..3707b70 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -4,3 +4,5 @@ protocol = "sparse" [target.x86_64-pc-windows-gnu] rustflags = "-C link-args=-lssp" # Does not compile without this line +[target.aarch64-unknown-linux-gnu] +rustflags = "-C linker=aarch64-linux-gnu-gcc" \ No newline at end of file diff --git a/.github/workflows/build-push.yml b/.github/workflows/build-push.yml index b04fb13..8e143c3 100644 --- a/.github/workflows/build-push.yml +++ b/.github/workflows/build-push.yml @@ -1,34 +1,66 @@ -name: Build and push to repository +name: Build and push to registry on: push: - branches: [ "main" ] + branches: [ "main", "dev" ] + tags: [ "v*.*.*" ] + pull_request: + branches: [ "main", "dev" ] + workflow_dispatch: +permissions: + packages: write + contents: read + jobs: build-and-push: - name: Build Docker image and push to repository + name: Build Docker image and push to registry runs-on: ubuntu-latest - steps: - name: Checkout code uses: actions/checkout@v3 - + - name: Set up Docker buildx id: buildx uses: docker/setup-buildx-action@v2 - - - name: Login to repository + + - name: Login to GitHub's container registry + if: github.event_name != 'pull_request' uses: docker/login-action@v2 with: - registry: ${{ secrets.REGISTRY_URL }} - username: ${{ secrets.REGISTRY_USERNAME }} - password: ${{ secrets.REGISTRY_PASSWORD }} - + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: Entepotenz/change-string-case-action-min-dependencies@v1 # https://github.com/orgs/community/discussions/10553 + id: repo-uri-string + with: + string: ghcr.io/${{ github.repository }} + + - name: Generate image metadata + id: docker-meta # used in next step + uses: docker/metadata-action@v5 + with: + # list of Docker images to use as base name for tags + images: ${{ steps.repo-uri-string.outputs.lowercase }} + # Docker tags based on the following events/attributes + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=sha + - name: Build image and push to registry - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: . - file: ./Dockerfile - tags: | - ${{ secrets.REGISTRY_URL }}/spoticord/spoticord:latest - push: ${{ github.ref == 'refs/heads/main' }} + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.docker-meta.outputs.tags }} + labels: ${{ steps.docker-meta.outputs.labels }} + # Some basic caching of the layers... + cache-from: ${{ steps.repo-uri-string.outputs.lowercase }}:latest-cache + cache-to: ${{ steps.repo-uri-string.outputs.lowercase }}:latest-cache diff --git a/CHANGELOG.md b/CHANGELOG.md index a4666bd..8213056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,47 @@ # Changelog + +## 2.1.2 | September 28th 2023 + +### Changes +* Removed OpenSSL dependency +* Added aarch64 support +* Added cross compilation to Github Actions +* Added `dev` branch to Github Actions +* Removed hardcoded URL in the /join command +* Fixed an issue in /playing where the bot showed it was playing even though it was paused + +**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.1.1...v2.1.2 + +## 2.1.1 | September 23rd 2023 +Reduced the amount of CPU that the bot uses from ~15%-25% per user to 1%-2% per user (percentage per core, benched on an AMD Ryzen 9 5950X). + +### Changes +* Fixed issue #20 + +**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.1.0...v2.1.1 + +## 2.1.0 | September 20th 2023 +So, it's been a while since I worked on this project, and some bugs have since been discovered. +The main focus for this version is to stop using multiple processes for every player, and instead do everything in threads. + +### Changes + +- Remove metrics, as I wasn't using this feature anyways +- Bring back KV for storing total/active sessions, as prometheus is no longer being used +- Allocate new players in-memory, instead of using subprocesses +- Fix issue #17 +- Fix some issues with the auto-disconnect +- Removed the automatic device switching on bot join, which was causing some people to not be able to use the bot +- Force communication through the closest Spotify AP, reducing latency +- Potential jitter reduction +- Enable autoplay +- After skipping a song, you will no longer hear a tiny bit of the previous song after the silence + + +**Full Changelog**: https://github.com/SpoticordMusic/spoticord/compare/v2.0.0...v2.1.0 + +### Issues +- Currently, the CPU usage is much higher than it used to be. I really wanted to push this update out before taking the time to do some optimizations, as the bot and server are still easily able to hold up the limited amount of Spoticord users (and v2.0.0 was just falling apart). Issue is being tracked in #20 + ## 2.0.0 | June 8th 2023 -- Initial Release +- Initial Release \ No newline at end of file diff --git a/COMPILING.md b/COMPILING.md index 56c784d..2b9c7f8 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -27,17 +27,17 @@ sudo pacman -S base-devel sudo dnf install gcc ``` -Additionally, you will need to install CMake and OpenSSL (Linux only). On Windows, you can download CMake [here](https://cmake.org/download/). On Linux, you can use your package manager to install them: +Additionally, you will need to install CMake. On Windows, you can download CMake [here](https://cmake.org/download/). On Linux, you can use your package manager to install it: ```sh # Debian/Ubuntu -sudo apt install cmake libssl-dev +sudo apt install cmake # Arch -sudo pacman -S cmake openssl +sudo pacman -S cmake # Fedora -sudo dnf install cmake openssl-devel +sudo dnf install cmake ``` ## Compiling diff --git a/Cargo.lock b/Cargo.lock index 331a3af..0fe5cb3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -328,16 +328,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.4" @@ -541,9 +531,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fixedbitset" @@ -580,21 +570,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.0" @@ -940,19 +915,6 @@ dependencies = [ "tokio-rustls 0.24.1", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "iana-time-zone" version = "0.1.57" @@ -1409,24 +1371,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "native-tls" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" -dependencies = [ - "lazy_static", - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - [[package]] name = "nix" version = "0.23.2" @@ -1531,50 +1475,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" -[[package]] -name = "openssl" -version = "0.10.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" -dependencies = [ - "bitflags 2.4.0", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.37", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "ordered-float" version = "2.10.0" @@ -1827,8 +1727,6 @@ dependencies = [ "itoa", "percent-encoding", "ryu", - "sha1_smol", - "socket2 0.4.9", "url", ] @@ -1910,13 +1808,11 @@ dependencies = [ "http-body", "hyper", "hyper-rustls", - "hyper-tls", "ipnet", "js-sys", "log", "mime", "mime_guess", - "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -1926,7 +1822,6 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", - "tokio-native-tls", "tokio-rustls 0.24.1", "tokio-util", "tower-service", @@ -2068,15 +1963,6 @@ dependencies = [ "libsamplerate-sys", ] -[[package]] -name = "schannel" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" -dependencies = [ - "windows-sys", -] - [[package]] name = "scoped-tls" version = "1.0.1" @@ -2099,29 +1985,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.19" @@ -2274,12 +2137,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "shannon" version = "0.2.0" @@ -2398,12 +2255,13 @@ dependencies = [ [[package]] name = "spoticord" -version = "2.1.1" +version = "2.1.2" dependencies = [ "anyhow", "dotenv", "env_logger 0.10.0", "hex", + "lazy_static", "librespot", "log", "protobuf", @@ -2415,7 +2273,6 @@ dependencies = [ "serenity", "songbird", "thiserror", - "time", "tokio", "zerocopy 0.7.5", ] @@ -2543,9 +2400,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" +checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" dependencies = [ "deranged", "itoa", @@ -2556,15 +2413,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2614,16 +2471,6 @@ dependencies = [ "syn 2.0.37", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2888,12 +2735,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vergen" version = "3.2.0" diff --git a/Cargo.toml b/Cargo.toml index e60abe8..6005d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "spoticord" -version = "2.1.1" +version = "2.1.2" edition = "2021" rust-version = "1.65.0" @@ -16,18 +16,18 @@ anyhow = "1.0.75" dotenv = "0.15.0" env_logger = "0.10.0" hex = "0.4.3" +lazy_static = "1.4.0" librespot = { version = "0.4.2", default-features = false } log = "0.4.20" protobuf = "2.28.0" -redis = { version = "0.23.3", optional = true } -reqwest = "0.11.20" +redis = { version = "0.23.3", optional = true, default-features = false } +reqwest = { version = "0.11.20", default-features = false } samplerate = "0.2.4" serde = "1.0.188" serde_json = "1.0.107" -serenity = { version = "0.11.6", features = ["framework", "cache", "standard_framework"], default-features = false } -songbird = "0.3.2" +serenity = { version = "0.11.6", features = ["framework", "cache", "standard_framework", "rustls_backend", "gateway"], default-features = false } +songbird = { version = "0.3.2", features = ["driver", "serenity-rustls"], default-features = false } thiserror = "1.0.48" -time = "0.3.28" tokio = { version = "1.32.0", features = ["rt", "full"] } zerocopy = "0.7.5" diff --git a/Dockerfile b/Dockerfile index 266e340..38e72a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,44 @@ # Builder -FROM rust:1.72.1-buster as builder +FROM --platform=linux/amd64 rust:1.72.1-buster as builder WORKDIR /app # Add extra build dependencies here -RUN apt-get update && apt-get install -y cmake +RUN apt-get update && apt-get install -yqq \ + cmake gcc-aarch64-linux-gnu binutils-aarch64-linux-gnu COPY . . -# Remove `--features stats` if you want to deploy without stats collection -RUN cargo install --path . --features stats +RUN rustup target add x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu + +# Remove `--features=stats` if you want to deploy without stats collection +RUN cargo build --features=stats --release \ + --target=x86_64-unknown-linux-gnu --target=aarch64-unknown-linux-gnu # Runtime FROM debian:buster-slim -WORKDIR /app +ARG TARGETPLATFORM +ENV TARGETPLATFORM=$TARGETPLATFORM # Add extra runtime dependencies here -RUN apt-get update && apt-get install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/* +# RUN apt-get update && apt-get install -yqq --no-install-recommends \ +# openssl ca-certificates && rm -rf /var/lib/apt/lists/* -# Copy spoticord binary from builder -COPY --from=builder /usr/local/cargo/bin/spoticord ./spoticord +# Copy spoticord binaries from builder to /tmp +COPY --from=builder \ + /app/target/x86_64-unknown-linux-gnu/release/spoticord /tmp/x86_64 +COPY --from=builder \ + /app/target/aarch64-unknown-linux-gnu/release/spoticord /tmp/aarch64 -CMD ["./spoticord"] \ No newline at end of file +# Copy appropiate binary for target arch from /tmp +RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \ + cp /tmp/x86_64 /usr/local/bin/spoticord; \ + elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \ + cp /tmp/aarch64 /usr/local/bin/spoticord; \ + fi + +# Delete unused binaries +RUN rm -rvf /tmp/x86_64 /tmp/aarch64 + +ENTRYPOINT [ "/usr/local/bin/spoticord" ] diff --git a/src/bot/commands/core/link.rs b/src/bot/commands/core/link.rs index 14b238f..bf606d6 100644 --- a/src/bot/commands/core/link.rs +++ b/src/bot/commands/core/link.rs @@ -8,6 +8,7 @@ use serenity::{ use crate::{ bot::commands::{respond_message, CommandOutput}, + consts::SPOTICORD_ACCOUNTS_URL, database::{Database, DatabaseError}, utils::embed::{EmbedBuilder, Status}, }; @@ -39,8 +40,11 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO } if let Ok(request) = database.get_user_request(command.user.id.to_string()).await { - let base = std::env::var("SPOTICORD_ACCOUNTS_URL").expect("to be present"); - let link = format!("{}/spotify/{}", base, request.token); + let link = format!( + "{}/spotify/{}", + SPOTICORD_ACCOUNTS_URL.as_str(), + request.token + ); respond_message( &ctx, @@ -106,8 +110,11 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO .await { Ok(request) => { - let base = std::env::var("SPOTICORD_ACCOUNTS_URL").expect("to be present"); - let link = format!("{}/spotify/{}", base, request.token); + let link = format!( + "{}/spotify/{}", + SPOTICORD_ACCOUNTS_URL.as_str(), + request.token + ); respond_message( &ctx, diff --git a/src/bot/commands/music/join.rs b/src/bot/commands/music/join.rs index fcb5fcc..8c5cf5f 100644 --- a/src/bot/commands/music/join.rs +++ b/src/bot/commands/music/join.rs @@ -7,6 +7,7 @@ use serenity::{ use crate::{ bot::commands::{defer_message, respond_message, update_message, CommandOutput}, + consts::SPOTICORD_ACCOUNTS_URL, session::manager::{SessionCreateError, SessionManager}, utils::embed::{EmbedBuilder, Status}, }; @@ -241,7 +242,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO &command, EmbedBuilder::new() .title("Cannot join voice channel") - .description("You need to link your Spotify account. Use or go to [the accounts website](https://account.spoticord.com/) to get started.") + .description(format!("You need to link your Spotify account. Use or go to [the accounts website]({}) to get started.", SPOTICORD_ACCOUNTS_URL.as_str())) .status(Status::Error) .build(), ) @@ -255,7 +256,7 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO &command, EmbedBuilder::new() .title("Cannot join voice channel") - .description("Spoticord no longer has access to your Spotify account. Use or go to [the accounts website](https://account.spoticord.com/) to relink your Spotify account.") + .description(format!("Spoticord no longer has access to your Spotify account. Use or go to [the accounts website]({}) to relink your Spotify account.", SPOTICORD_ACCOUNTS_URL.as_str())) .status(Status::Error) .build(), ).await; diff --git a/src/bot/commands/music/playing.rs b/src/bot/commands/music/playing.rs index 9dbe5ad..f0aae85 100644 --- a/src/bot/commands/music/playing.rs +++ b/src/bot/commands/music/playing.rs @@ -30,20 +30,22 @@ pub const NAME: &str = "playing"; pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandOutput { Box::pin(async move { - let not_playing = async { - respond_message( - &ctx, - &command, - EmbedBuilder::new() - .title("Cannot get track info") - .icon_url("https://spoticord.com/forbidden.png") - .description("I'm currently not playing any music in this server") - .status(Status::Error) - .build(), - true, - ) - .await; - }; + macro_rules! not_playing { + () => { + respond_message( + &ctx, + &command, + EmbedBuilder::new() + .title("Cannot get track info") + .icon_url("https://spoticord.com/forbidden.png") + .description("I'm currently not playing any music in this server") + .status(Status::Error) + .build(), + true, + ) + .await; + }; + } let data = ctx.data.read().await; let session_manager = data @@ -51,62 +53,50 @@ pub fn command(ctx: Context, command: ApplicationCommandInteraction) -> CommandO .expect("to contain a value") .clone(); - let session = match session_manager + let Some(session) = session_manager .get_session(command.guild_id.expect("to contain a value")) .await - { - Some(session) => session, - None => { - not_playing.await; + else { + not_playing!(); - return; - } + return; }; - let owner = match session.owner().await { - Some(owner) => owner, - None => { - not_playing.await; + let Some(owner) = session.owner().await else { + not_playing!(); - return; - } + return; }; // Get Playback Info from session - let pbi = match session.playback_info().await { - Some(pbi) => pbi, - None => { - not_playing.await; + let Some(pbi) = session.playback_info().await else { + not_playing!(); - return; - } + return; }; // Get owner of session - let owner = match utils::discord::get_user(&ctx, owner).await { - Some(user) => user, - None => { - // This shouldn't happen + let Some(owner) = utils::discord::get_user(&ctx, owner).await else { + // This shouldn't happen - error!("Could not find user with ID: {owner}"); + error!("Could not find user with ID: {owner}"); - respond_message( - &ctx, - &command, - EmbedBuilder::new() - .title("[INTERNAL ERROR] Cannot get track info") - .description(format!( - "Could not find user with ID `{}`\nThis is an issue with the bot!", - owner - )) - .status(Status::Error) - .build(), - true, - ) - .await; + respond_message( + &ctx, + &command, + EmbedBuilder::new() + .title("[INTERNAL ERROR] Cannot get track info") + .description(format!( + "Could not find user with ID `{}`\nThis is an issue with the bot!", + owner + )) + .status(Status::Error) + .build(), + true, + ) + .await; - return; - } + return; }; // Get metadata @@ -188,48 +178,39 @@ pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) -> .clone(); // Check if session still exists - let mut session = match session_manager + let Some(mut session) = session_manager .get_session(interaction.guild_id.expect("to contain a value")) .await - { - Some(session) => session, - None => { - error_edit( - "Cannot perform action", - "I'm currently not playing any music in this server", - ) - .await; + else { + error_edit( + "Cannot perform action", + "I'm currently not playing any music in this server", + ) + .await; - return; - } + return; }; // Check if the session contains an owner - let owner = match session.owner().await { - Some(owner) => owner, - None => { - error_edit( - "Cannot change playback state", - "I'm currently not playing any music in this server", - ) - .await; + let Some(owner) = session.owner().await else { + error_edit( + "Cannot change playback state", + "I'm currently not playing any music in this server", + ) + .await; - return; - } + return; }; // Get Playback Info from session - let pbi = match session.playback_info().await { - Some(pbi) => pbi, - None => { - error_edit( - "Cannot change playback state", - "I'm currently not playing any music in this server", - ) - .await; + let Some(pbi) = session.playback_info().await else { + error_edit( + "Cannot change playback state", + "I'm currently not playing any music in this server", + ) + .await; - return; - } + return; }; // Check if the user is the owner of the session @@ -244,30 +225,27 @@ pub fn component(ctx: Context, mut interaction: MessageComponentInteraction) -> } // Get owner of session - let owner = match utils::discord::get_user(&ctx, owner).await { - Some(user) => user, - None => { - // This shouldn't happen + let Some(owner) = utils::discord::get_user(&ctx, owner).await else { + // This shouldn't happen - error!("Could not find user with ID: {owner}"); + error!("Could not find user with ID: {owner}"); - respond_component_message( - &ctx, - &interaction, - EmbedBuilder::new() - .title("[INTERNAL ERROR] Cannot get track info") - .description(format!( - "Could not find user with ID `{}`\nThis is an issue with the bot!", - owner - )) - .status(Status::Error) - .build(), - true, - ) - .await; + respond_component_message( + &ctx, + &interaction, + EmbedBuilder::new() + .title("[INTERNAL ERROR] Cannot get track info") + .description(format!( + "Could not find user with ID `{}`\nThis is an issue with the bot!", + owner + )) + .status(Status::Error) + .build(), + true, + ) + .await; - return; - } + return; }; // Send the desired command to the session @@ -370,34 +348,28 @@ async fn update_embed(interaction: &mut MessageComponentInteraction, ctx: &Conte .clone(); // Check if session still exists - let session = match session_manager + let Some(session) = session_manager .get_session(interaction.guild_id.expect("to contain a value")) .await - { - Some(session) => session, - None => { - error_edit( - "Cannot perform action", - "I'm currently not playing any music in this server", - ) - .await; + else { + error_edit( + "Cannot perform action", + "I'm currently not playing any music in this server", + ) + .await; - return; - } + return; }; // Get Playback Info from session - let pbi = match session.playback_info().await { - Some(pbi) => pbi, - None => { - error_edit( - "Cannot change playback state", - "I'm currently not playing any music in this server", - ) - .await; + let Some(pbi) = session.playback_info().await else { + error_edit( + "Cannot change playback state", + "I'm currently not playing any music in this server", + ) + .await; - return; - } + return; }; let (title, description, thumbnail) = get_metadata(&pbi); diff --git a/src/bot/events.rs b/src/bot/events.rs index 3668150..f70639a 100644 --- a/src/bot/events.rs +++ b/src/bot/events.rs @@ -54,23 +54,22 @@ impl EventHandler for Handler { // INTERACTION_CREATE event, emitted when the bot receives an interaction (slash command, button, etc.) async fn interaction_create(&self, ctx: Context, interaction: Interaction) { match interaction { - Interaction::ApplicationCommand(command) => self.handle_command(ctx, command).await, - Interaction::MessageComponent(component) => self.handle_component(ctx, component).await, + Interaction::ApplicationCommand(command) => handle_command(ctx, command).await, + Interaction::MessageComponent(component) => handle_component(ctx, component).await, _ => {} } } } -impl Handler { - async fn handle_command(&self, ctx: Context, command: ApplicationCommandInteraction) { - enforce_guild!(command); +async fn handle_command(ctx: Context, command: ApplicationCommandInteraction) { + enforce_guild!(command); - // Commands must only be executed inside of guilds + // Commands must only be executed inside of guilds - let guild_id = match command.guild_id { - Some(guild_id) => guild_id, - None => { - if let Err(why) = command + let guild_id = match command.guild_id { + Some(guild_id) => guild_id, + None => { + if let Err(why) = command .create_interaction_response(&ctx.http, |response| { response .kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource) @@ -82,32 +81,32 @@ impl Handler { error!("Failed to send run-in-guild-only error message: {}", why); } - return; - } - }; + return; + } + }; - trace!( - "Received command interaction: command={} user={} guild={}", - command.data.name, - command.user.id, - guild_id - ); + trace!( + "Received command interaction: command={} user={} guild={}", + command.data.name, + command.user.id, + guild_id + ); - let data = ctx.data.read().await; - let command_manager = data.get::().expect("to contain a value"); + let data = ctx.data.read().await; + let command_manager = data.get::().expect("to contain a value"); - command_manager.execute_command(&ctx, command).await; - } + command_manager.execute_command(&ctx, command).await; +} - async fn handle_component(&self, ctx: Context, component: MessageComponentInteraction) { - enforce_guild!(component); +async fn handle_component(ctx: Context, component: MessageComponentInteraction) { + enforce_guild!(component); - // Components can only be interacted with inside of guilds + // Components can only be interacted with inside of guilds - let guild_id = match component.guild_id { - Some(guild_id) => guild_id, - None => { - if let Err(why) = component + let guild_id = match component.guild_id { + Some(guild_id) => guild_id, + None => { + if let Err(why) = component .create_interaction_response(&ctx.http, |response| { response .kind(serenity::model::prelude::interaction::InteractionResponseType::ChannelMessageWithSource) @@ -119,20 +118,19 @@ impl Handler { error!("Failed to send run-in-guild-only error message: {}", why); } - return; - } - }; + return; + } + }; - trace!( - "Received component interaction: command={} user={} guild={}", - component.data.custom_id, - component.user.id, - guild_id - ); + trace!( + "Received component interaction: command={} user={} guild={}", + component.data.custom_id, + component.user.id, + guild_id + ); - let data = ctx.data.read().await; - let command_manager = data.get::().expect("to contain a value"); + let data = ctx.data.read().await; + let command_manager = data.get::().expect("to contain a value"); - command_manager.execute_component(&ctx, component).await; - } + command_manager.execute_component(&ctx, component).await; } diff --git a/src/consts.rs b/src/consts.rs index c050977..d3326bf 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,3 +1,5 @@ +use lazy_static::lazy_static; + #[cfg(not(debug_assertions))] pub const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -8,3 +10,18 @@ pub const MOTD: &str = "some good 'ol music"; /// The time it takes for Spoticord to disconnect when no music is being played pub const DISCONNECT_TIME: u64 = 5 * 60; + +lazy_static! { + pub static ref DISCORD_TOKEN: String = + std::env::var("DISCORD_TOKEN").expect("missing DISCORD_TOKEN environment variable"); + pub static ref DATABASE_URL: String = + std::env::var("DATABASE_URL").expect("missing DATABASE_URL environment variable"); + pub static ref SPOTICORD_ACCOUNTS_URL: String = std::env::var("SPOTICORD_ACCOUNTS_URL") + .expect("missing SPOTICORD_ACCOUNTS_URL environment variable"); +} + +#[cfg(feature = "stats")] +lazy_static! { + pub static ref KV_URL: String = + std::env::var("KV_URL").expect("missing KV_URL environment variable"); +} diff --git a/src/main.rs b/src/main.rs index 89593f7..f8b6f01 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,18 @@ use dotenv::dotenv; -use crate::{bot::commands::CommandManager, database::Database, session::manager::SessionManager}; +#[cfg(feature = "stats")] +use crate::consts::KV_URL; + +use crate::{ + bot::commands::CommandManager, + consts::{DATABASE_URL, DISCORD_TOKEN, MOTD}, + database::Database, + session::manager::SessionManager, +}; use log::*; use serenity::{framework::StandardFramework, prelude::GatewayIntents, Client}; use songbird::SerenityInit; -use std::{any::Any, env, process::exit}; +use std::{any::Any, process::exit}; #[cfg(unix)] use tokio::signal::unix::SignalKind; @@ -41,7 +49,7 @@ async fn main() { env_logger::init(); info!("It's a good day"); - info!(" - Spoticord {}", time::OffsetDateTime::now_utc().year()); + info!(" - Spoticord, {}", MOTD); let result = dotenv(); @@ -54,19 +62,14 @@ async fn main() { warn!("No .env file found, expecting all necessary environment variables"); } - let token = env::var("DISCORD_TOKEN").expect("a token in the environment"); - let db_url = env::var("DATABASE_URL").expect("a database URL in the environment"); - #[cfg(feature = "stats")] - let stats_manager = - StatsManager::new(env::var("KV_URL").expect("a redis URL in the environment")) - .expect("Failed to connect to redis"); + let stats_manager = StatsManager::new(KV_URL.as_str()).expect("Failed to connect to redis"); let session_manager = SessionManager::new(); // Create client let mut client = Client::builder( - token, + DISCORD_TOKEN.as_str(), GatewayIntents::GUILDS | GatewayIntents::GUILD_VOICE_STATES, ) .event_handler(crate::bot::events::Handler) @@ -78,16 +81,13 @@ async fn main() { { let mut data = client.data.write().await; - data.insert::(Database::new(db_url, None)); + data.insert::(Database::new(DATABASE_URL.as_str(), None)); data.insert::(CommandManager::new()); data.insert::(session_manager.clone()); } let shard_manager = client.shard_manager.clone(); - #[cfg(feature = "stats")] - let cache = client.cache_and_http.cache.clone(); - #[cfg(unix)] let mut term: Option> = Some(Box::new( tokio::signal::unix::signal(SignalKind::terminate()) @@ -104,13 +104,8 @@ async fn main() { _ = tokio::time::sleep(std::time::Duration::from_secs(60)) => { #[cfg(feature = "stats")] { - let guild_count = cache.guilds().len(); let active_count = session_manager.get_active_session_count().await; - if let Err(why) = stats_manager.set_server_count(guild_count) { - error!("Failed to update server count: {why}"); - } - if let Err(why) = stats_manager.set_active_count(active_count) { error!("Failed to update active count: {why}"); } diff --git a/src/player/mod.rs b/src/player/mod.rs index b651852..ef03233 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -318,13 +318,13 @@ impl PlayerTask { match pbi.as_mut() { Some(pbi) => { pbi.update_track(spotify_id, current); - pbi.update_pos_dur(position_ms, duration_ms, true); + pbi.update_pos_dur(position_ms, duration_ms, playing); } None => { *pbi = Some(PlaybackInfo::new( duration_ms, position_ms, - true, + playing, current, spotify_id, )); diff --git a/src/session/manager.rs b/src/session/manager.rs index 5d3d186..d639b2d 100644 --- a/src/session/manager.rs +++ b/src/session/manager.rs @@ -56,11 +56,9 @@ impl InnerSessionManager { session: SpoticordSession, guild_id: GuildId, owner_id: UserId, - ) -> Result<(), SessionCreateError> { + ) { self.sessions.insert(guild_id, session); self.owner_map.insert(owner_id, guild_id); - - Ok(()) } /// Remove a session @@ -147,7 +145,9 @@ impl SessionManager { .write() .await .create_session(session, guild_id, owner_id) - .await + .await; + + Ok(()) } /// Remove a session diff --git a/src/session/mod.rs b/src/session/mod.rs index 5a00063..a0f0ca5 100644 --- a/src/session/mod.rs +++ b/src/session/mod.rs @@ -36,6 +36,12 @@ use tokio::sync::{Mutex, RwLockReadGuard, RwLockWriteGuard}; #[derive(Clone)] pub struct SpoticordSession(Arc>); +impl Drop for SpoticordSession { + fn drop(&mut self) { + log::trace!("drop SpoticordSession"); + } +} + struct InnerSpoticordSession { owner: Option, guild_id: GuildId, @@ -97,7 +103,11 @@ impl SpoticordSession { }; let mut instance = Self(Arc::new(RwLock::new(inner))); - instance.create_player(ctx).await?; + if let Err(why) = instance.create_player(ctx).await { + songbird.remove(guild_id).await.ok(); + + return Err(why); + } let mut call = call.lock().await; @@ -336,6 +346,8 @@ impl SpoticordSession { timer.tick().await; timer.tick().await; + trace!("Ring ring, time to check :)"); + // Make sure this task has not been aborted, if it has this will automatically stop execution. tokio::task::yield_now().await; @@ -345,6 +357,8 @@ impl SpoticordSession { .map(|pbi| pbi.is_playing) .unwrap_or(false); + trace!("is_playing = {is_playing}"); + if !is_playing { info!("Player is not playing, disconnecting"); session @@ -499,19 +513,19 @@ impl InnerSpoticordSession { .remove_session(self.guild_id, self.owner) .await; - let mut call = self.call.lock().await; - if let Some(track) = self.track.take() { if let Err(why) = track.stop() { error!("Failed to stop track: {:?}", why); } - } + }; - call.remove_all_global_events(); + let mut call = self.call.lock().await; if let Err(why) = call.leave().await { error!("Failed to leave voice channel: {:?}", why); } + + call.remove_all_global_events(); } } @@ -521,10 +535,12 @@ impl EventHandler for SpoticordSession { match ctx { EventContext::DriverDisconnect(_) => { debug!("Driver disconnected, leaving voice channel"); + trace!("Arc strong count: {}", Arc::strong_count(&self.0)); self.disconnect().await; } EventContext::ClientDisconnect(who) => { trace!("Client disconnected, {}", who.user_id.to_string()); + trace!("Arc strong count: {}", Arc::strong_count(&self.0)); if let Some(session) = self .session_manager() @@ -545,3 +561,9 @@ impl EventHandler for SpoticordSession { return None; } } + +impl Drop for InnerSpoticordSession { + fn drop(&mut self) { + log::trace!("drop InnerSpoticordSession"); + } +} diff --git a/src/stats.rs b/src/stats.rs index 7b54e2c..7dde73a 100644 --- a/src/stats.rs +++ b/src/stats.rs @@ -12,12 +12,6 @@ impl StatsManager { Ok(StatsManager { redis }) } - pub fn set_server_count(&self, count: usize) -> Result<()> { - let mut con = self.redis.get_connection()?; - - con.set("sc-bot-total-servers", count.to_string()) - } - pub fn set_active_count(&self, count: usize) -> Result<()> { let mut con = self.redis.get_connection()?;