Compare commits

..

No commits in common. 'main' and 'v0.6.2' have entirely different histories.
main ... v0.6.2

@ -1,2 +0,0 @@
target
.env

@ -41,7 +41,7 @@ jobs:
- name: Move binaries - name: Move binaries
run: mv target/x86_64-unknown-linux-gnu/release/tobi-rs target/tobi-rs-linux-x86_64 run: mv target/x86_64-unknown-linux-gnu/release/tobi-rs target/tobi-rs-linux-x86_64
- name: Sign artifact - name: Sign artifact
run: gpg --batch --yes --pinentry-mode loopback --passphrase "${{ secrets.PASSPHRASE }}" --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64.sig target/tobi-rs-linux-x86_64 run: gpg --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64.sig target/tobi-rs-linux-x86_64
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:

@ -1,56 +0,0 @@
name: Build Container
on:
workflow_dispatch:
push:
tags:
- "v*"
schedule:
# daily builds to always include patches in the docker image
- cron: '0 4 * * *'
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /var/lib/containers/
key: ${{ runner.os }}-podman-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-podman-
- name: Build
id: build-image
uses: redhat-actions/buildah-build@v2
with:
context: .
layers: true
containerfiles: ./Containerfile
platforms: ${{github.event.inputs.platforms}}
image: trivernis/tobi
- name: Login to DockerHub
uses: redhat-actions/podman-login@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
registry: docker.io
- name: Push
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-image.outputs.image }}
tags: ${{ steps.build-image.outputs.tags }}
registry: docker.io

@ -1,4 +1,4 @@
name: Check name: Build and Test
on: on:
workflow_dispatch: workflow_dispatch:
@ -32,14 +32,8 @@ jobs:
restore-keys: | restore-keys: |
${{ runner.os }}-cargo- ${{ runner.os }}-cargo-
- name: Build
- name: Run Rustfmt run: cargo build --verbose
uses: actions-rust-lang/rustfmt@v1.0.0
- name: Run Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Test coreutils - name: Test coreutils
run: cargo test --verbose --package bot-coreutils run: cargo test --verbose --package bot-coreutils
@ -61,7 +55,7 @@ jobs:
passphrase: ${{ secrets.PASSPHRASE }} passphrase: ${{ secrets.PASSPHRASE }}
- name: Sign artifact - name: Sign artifact
run: gpg --batch --yes --pinentry-mode loopback --passphrase "${{ secrets.PASSPHRASE }}" --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64_debug.sig target/tobi-rs-linux-x86_64_debug run: gpg --detach-sign --sign --armor --default-key steps.import_gpg.outputs.keyid --output target/tobi-rs-linux-x86_64_debug.sig target/tobi-rs-linux-x86_64_debug
- name: Upload artifacts - name: Upload artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

@ -1,39 +0,0 @@
version: 1
when:
- event: [pull_request]
- event: push
branch:
- ${CI_REPO_DEFAULT_BRANCH}
- release/*
- fix/*
steps:
test:
image: rust:alpine
commands:
- apk add --no-cache --force-overwrite \
build-base \
openssl-dev \
libopusenc-dev \
libpq-dev \
curl \
bash
- rustup default stable
- rustup component add clippy --toolchain stable-x86_64-unknown-linux-musl
- cargo clippy
- cargo test --verbose --package bot-coreutils
- cargo test --verbose --package bot-database
- cargo test --verbose
build:
image: rust:alpine
commands:
- apk add --no-cache --force-overwrite \
build-base \
openssl-dev \
libopusenc-dev \
libpq-dev \
curl \
bash
- cargo build
when:
- event: [pull_request]

@ -1,20 +0,0 @@
version: 1
when:
- event: [tag]
branch:
- ${CI_REPO_DEFAULT_BRANCH}
steps:
build:
image: woodpeckerci/plugin-docker-buildx
secrets: [forgejo_token]
settings:
dockerfile: Containerfile
tag: ${CI_COMMIT_TAG##v}
repo: git.trivernis.net/trivernis/2b-rs
registry: git.trivernis.net
platforms: linux/amd64
username:
from_secret: forgejo_id
password:
from_secret: forgejo_token

4748
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -1,9 +1,6 @@
[workspace]
members=["bot-coreutils", "bot-database", "bot-database/migration", "."]
[package] [package]
name = "tobi-rs" name = "tobi-rs"
version = "0.11.4" version = "0.6.2"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018" edition = "2018"
@ -15,41 +12,28 @@ panic = 'abort'
[dependencies] [dependencies]
bot-database = {path="./bot-database"} bot-database = {path="./bot-database"}
bot-coreutils = {path="./bot-coreutils"} bot-coreutils = {path="./bot-coreutils"}
serenity = "0.11.5" bot-serenityutils = {path="./bot-serenityutils"}
serenity = "0.10.5"
dotenv = "0.15.0" dotenv = "0.15.0"
serde_derive = "1.0.145" tokio = { version = "1.4.0", features = ["macros", "rt-multi-thread"] }
serde = "1.0.145" serde_derive = "1.0.125"
thiserror = "1.0.37" serde = "1.0.125"
minecraft-data-rs = "0.5.0" thiserror = "1.0.24"
serde_json = "1.0.86" minecraft-data-rs = "0.2.0"
rand = "0.8.5" songbird = "0.1.5"
regex = "1.6.0" serde_json = "1.0.64"
aspotify = "0.7.1" rand = "0.8.3"
regex = "1.4.5"
aspotify = "0.7.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
futures = "0.3.24" futures = "0.3.13"
chrono = "0.4.22" log = "0.4.14"
sysinfo = "0.26.4" fern = "0.6.0"
reqwest = "0.11.12" chrono = "0.4.19"
chrono-tz = "0.6.3" colored = "2.0.0"
sauce-api = "1.0.0" sysinfo = "0.16.5"
rustc_version_runtime = "0.2.1" reqwest = "0.11.2"
chrono-tz = "0.5.3"
sauce-api = "0.7.1"
rustc_version_runtime = "0.2.0"
trigram = "0.4.4" trigram = "0.4.4"
typemap_rev = "0.2.0"
youtube-metadata = "0.2.0"
xkcd-search = "0.1.2"
animethemes-rs = "0.4.5"
build-time = "0.1.2"
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing = "0.1.37"
serenity-additions = "0.3.4"
[dependencies.songbird]
version = "0.3.0"
features = ["yt-dlp"]
[dependencies.tokio]
version = "1.21.2"
features = ["macros", "rt-multi-thread"]
# [patch.crates-io]
# serenity-additions = { path = "../serenity-additions" }

@ -1,45 +0,0 @@
ARG BASE_IMAGE=docker.io/alpine:edge
FROM ${BASE_IMAGE} AS build_base
RUN apk update
RUN apk add --no-cache --force-overwrite \
build-base \
openssl-dev \
libopusenc-dev \
libpq-dev \
curl \
bash
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
RUN rm -rf /var/lib/{cache,log}/ /var/cache
FROM build_base AS builder
ENV RUSTFLAGS="-C target-feature=-crt-static"
WORKDIR /usr/src
RUN cargo new tobi
WORKDIR /usr/src/tobi
COPY Cargo.toml Cargo.lock ./
COPY src ./src
COPY bot-coreutils ./bot-coreutils
COPY bot-database ./bot-database
RUN cargo build --release
RUN mkdir /tmp/tobi
RUN cp target/release/tobi-rs /tmp/tobi/
FROM ${BASE_IMAGE} AS runtime-base
RUN apk update
RUN apk add --no-cache --force-overwrite \
openssl \
libopusenc \
libpq \
python3 \
py3-pip \
qalc \
ffmpeg \
bash
RUN pip3 install yt-dlp --break-system-packages
RUN rm -rf /var/lib/{cache,log}/ /var/cache
FROM runtime-base
COPY --from=builder /tmp/tobi/tobi-rs .
ENTRYPOINT ["/tobi-rs"]

@ -63,4 +63,4 @@ The required values are:
## License ## License
See LICENSE.md It's GPL 3.0

@ -0,0 +1,314 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bot-coreutils"
version = "0.1.1"
dependencies = [
"log",
"mime_guess",
"rand",
"tokio",
"url",
]
[[package]]
name = "bytes"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "getrandom"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "idna"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89829a5d69c23d348314a7ac337fe39173b61149a9864deabd260983aed48c21"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "libc"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "matches"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08"
[[package]]
name = "memchr"
version = "2.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee1c47aaa256ecabcaea351eae4a9b01ef39ed810004e298d2511ed284b1525"
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2684d4c2e97d99848d30b324b00c8fcc7e5c897b7cbb5819b09e7c90e8baf212"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "mio"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
dependencies = [
"libc",
"log",
"miow",
"ntapi",
"winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
]
[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44"
dependencies = [
"winapi",
]
[[package]]
name = "once_cell"
version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
[[package]]
name = "ppv-lite86"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "rand"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ef9e7e66b4468674bfcb0c81af8b7fa0bb154fa9f28eb840da5c447baeb8d7e"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
"rand_hc",
]
[[package]]
name = "rand_chacha"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e12735cf05c9e10bf21534da50a147b924d555dc7a547c42e6bb2d5b6017ae0d"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34cf66eb183df1c5876e2dcf6b13d57340741e8dc255b48e40a26de954d06ae7"
dependencies = [
"getrandom",
]
[[package]]
name = "rand_hc"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3190ef7066a446f2e7f42e239d161e905420ccab01eb967c9eb27d21b2322a73"
dependencies = [
"rand_core",
]
[[package]]
name = "signal-hook-registry"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
dependencies = [
"libc",
]
[[package]]
name = "tinyvec"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
"mio",
"once_cell",
"pin-project-lite",
"signal-hook-registry",
"winapi",
]
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-bidi"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0"
dependencies = [
"matches",
]
[[package]]
name = "unicode-normalization"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ccd964113622c8e9322cfac19eb1004a07e636c545f325da085d5cdde6f1f8b"
dependencies = [
"form_urlencoded",
"idna",
"matches",
"percent-encoding",
]
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

@ -7,8 +7,8 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1.21.2", features = ["process", "io-util"] } tokio = {version = "1.4.0", features = ["process", "io-util"]}
log = "0.4.17" log = "0.4.14"
url = "2.3.1" url = "2.2.1"
mime_guess = "2.0.4" mime_guess = "2.0.3"
rand = "0.8.5" rand = "0.8.3"

@ -11,7 +11,7 @@ pub async fn run_command_async(command: &str, args: &[&str]) -> io::Result<Strin
let stdout = String::from_utf8_lossy(&process_output.stdout[..]); let stdout = String::from_utf8_lossy(&process_output.stdout[..]);
if stderr.len() != 0 { if stderr.len() != 0 {
log::trace!("STDERR of command {}: {}", command, stderr); log::debug!("STDERR of command {}: {}", command, stderr);
} }
log::trace!("Command output is {}", stdout); log::trace!("Command output is {}", stdout);

@ -0,0 +1,467 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
[[package]]
name = "async-trait"
version = "0.1.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36ea56748e10732c49404c153638a15ec3d6211ec5ff35d9bb20e13b93576adf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
[[package]]
name = "bot-database"
version = "0.4.0"
dependencies = [
"chrono",
"diesel",
"diesel_migrations",
"dotenv",
"log",
"r2d2",
"thiserror",
"tokio-diesel",
]
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"time",
"winapi",
]
[[package]]
name = "diesel"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "047bfc4d5c3bd2ef6ca6f981941046113524b9a9f9a7cbdfdd7ff40f58e6f542"
dependencies = [
"bitflags",
"byteorder",
"chrono",
"diesel_derives",
"pq-sys",
"r2d2",
]
[[package]]
name = "diesel_derives"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "diesel_migrations"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf3cde8413353dc7f5d72fa8ce0b99a560a359d2c5ef1e5817ca731cd9008f4c"
dependencies = [
"migrations_internals",
"migrations_macros",
]
[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]]
name = "futures"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d5813545e459ad3ca1bff9915e9ad7f1a47dc6a91b627ce321d5863b7dd253"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce79c6a52a299137a6013061e0cf0e688fce5d7f1bc60125f520912fdb29ec25"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "098cd1c6dda6ca01650f1a37a794245eb73181d0d4d4e955e2f3c37db7af1815"
[[package]]
name = "futures-io"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "365a1a1fb30ea1c03a830fdb2158f5236833ac81fa0ad12fe35b29cddc35cb04"
[[package]]
name = "futures-sink"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5629433c555de3d82861a7a4e3794a4c40040390907cfbfd7143a92a426c23"
[[package]]
name = "futures-task"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba7aa51095076f3ba6d9a1f702f74bd05ec65f555d70d2033d55ba8d69f581bc"
[[package]]
name = "futures-util"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c144ad54d60f23927f0a6b6d816e4271278b64f005ad65e4e35291d2de9c025"
dependencies = [
"futures-core",
"futures-sink",
"futures-task",
"pin-project-lite",
"pin-utils",
]
[[package]]
name = "hermit-abi"
version = "0.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c"
dependencies = [
"libc",
]
[[package]]
name = "instant"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
dependencies = [
"cfg-if",
]
[[package]]
name = "libc"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9385f66bf6105b241aa65a61cb923ef20efc665cb9f9bb50ac2f0c4b7f378d41"
[[package]]
name = "lock_api"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3c91c24eae6777794bb1997ad98bbb87daf92890acab859f7eaa4320333176"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "migrations_internals"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b4fc84e4af020b837029e017966f86a1c2d5e83e64b589963d5047525995860"
dependencies = [
"diesel",
]
[[package]]
name = "migrations_macros"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9753f12909fd8d923f75ae5c3258cae1ed3c8ec052e1b38c93c21a6d157f789c"
dependencies = [
"migrations_internals",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "num-integer"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "parking_lot"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall",
"smallvec",
"winapi",
]
[[package]]
name = "pin-project-lite"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pq-sys"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
dependencies = [
"vcpkg",
]
[[package]]
name = "proc-macro2"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r2d2"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "redox_syscall"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94341e4e44e24f6b591b59e47a8a027df12e008d73fd5672dbea9cc22f4507d9"
dependencies = [
"bitflags",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "smallvec"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "syn"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48fe99c6bd8b1cc636890bcc071842de909d902c81ac7dab53ba33c421ab8ffb"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
dependencies = [
"libc",
"wasi",
"winapi",
]
[[package]]
name = "tokio"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "134af885d758d645f0f0505c9a8b3f9bf8a348fd822e112ab5248138348f1722"
dependencies = [
"autocfg",
"num_cpus",
"pin-project-lite",
]
[[package]]
name = "tokio-diesel"
version = "0.3.0"
source = "git+https://github.com/Trivernis/tokio-diesel#f4af42558246ab323600622ba8d08803d3c18842"
dependencies = [
"async-trait",
"diesel",
"futures",
"r2d2",
"tokio",
]
[[package]]
name = "unicode-xid"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "vcpkg"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b00bca6106a5e23f3eee943593759b7fcddb00554332e856d990c893966879fb"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

@ -1,6 +1,6 @@
[package] [package]
name = "bot-database" name = "bot-database"
version = "0.6.0" version = "0.5.0"
authors = ["trivernis <trivernis@protonmail.com>"] authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018" edition = "2018"
@ -8,13 +8,10 @@ edition = "2018"
[dependencies] [dependencies]
dotenv = "0.15.0" dotenv = "0.15.0"
chrono = "0.4.22" chrono = "0.4.19"
thiserror = "1.0.37" thiserror = "1.0.24"
tracing = "0.1.37" diesel = {version="1.4.6", features=["postgres", "r2d2", "chrono"]}
log = "0.4.14"
[dependencies.sea-orm] diesel_migrations = "1.4.0"
version = "0.9.3" r2d2 = "0.8.9"
features = ["runtime-tokio-native-tls", "sqlx-postgres"] tokio-diesel = {git = "https://github.com/Trivernis/tokio-diesel"}
[dependencies.migration]
path = "./migration"

@ -0,0 +1,5 @@
# For documentation on how to configure this file,
# see diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/schema.rs"

@ -1,13 +0,0 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
sea-orm-migration = "0.9.3"
tokio = { version = "1.21.2", features = ["rt", "net", "tracing"] }

@ -1,37 +0,0 @@
# Running Migrator CLI
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

@ -1,16 +0,0 @@
pub use sea_orm_migration::prelude::*;
mod m20220029_164527_change_timestamp_format;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20220029_164527_change_timestamp_format::Migration),
]
}
}

@ -1,82 +0,0 @@
use crate::{DbErr, Table};
use sea_orm_migration::prelude::*;
#[derive(Iden)]
pub enum Statistics {
Table,
ExecutedAt,
}
#[derive(Iden)]
pub enum EphemeralMessages {
Table,
Timeout,
}
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220029_164527_change_timestamp_format"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Statistics::Table)
.modify_column(
ColumnDef::new(Statistics::ExecutedAt)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(EphemeralMessages::Table)
.modify_column(
ColumnDef::new(EphemeralMessages::Timeout)
.timestamp_with_time_zone()
.not_null(),
)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Statistics::Table)
.modify_column(
ColumnDef::new(Statistics::ExecutedAt)
.timestamp()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.alter_table(
Table::alter()
.table(EphemeralMessages::Table)
.modify_column(
ColumnDef::new(EphemeralMessages::Timeout)
.timestamp()
.not_null(),
)
.to_owned(),
)
.await?;
Ok(())
}
}

@ -1,287 +0,0 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
#[derive(Iden)]
pub enum EphemeralMessages {
Table,
ChannelId,
MessageId,
Timeout,
}
#[derive(Iden)]
pub enum GuildPlaylists {
Table,
GuildId,
Name,
Url,
}
#[derive(Iden)]
pub enum GuildSettings {
Table,
GuildId,
Key,
Value,
}
#[derive(Iden)]
pub enum Media {
Table,
Id,
Category,
Name,
Url,
}
#[derive(Iden)]
pub enum Statistics {
Table,
Id,
Version,
Command,
ExecutedAt,
Success,
ErrorMsg,
}
#[derive(Iden)]
pub enum YoutubeSongs {
Table,
Id,
SpotifyId,
Artist,
Title,
Album,
Url,
Score,
}
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220101_000001_create_table"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(ephemeral_messages()).await?;
manager.create_table(guild_playlists()).await?;
manager.create_table(guild_settings()).await?;
manager.create_table(media()).await?;
manager.create_table(statistics()).await?;
manager.create_table(youtube_songs()).await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(EphemeralMessages::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(GuildPlaylists::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(GuildSettings::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Media::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Statistics::Table).to_owned())
.await?;
manager
.drop_table(Table::drop().table(YoutubeSongs::Table).to_owned())
.await?;
Ok(())
}
}
fn ephemeral_messages() -> TableCreateStatement {
Table::create()
.table(EphemeralMessages::Table)
.if_not_exists()
.col(
ColumnDef::new(EphemeralMessages::ChannelId)
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(EphemeralMessages::MessageId)
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(EphemeralMessages::Timeout)
.timestamp()
.not_null(),
)
.primary_key(
Index::create()
.col(EphemeralMessages::ChannelId)
.col(EphemeralMessages::MessageId),
)
.to_owned()
}
fn guild_playlists() -> TableCreateStatement {
Table::create()
.table(GuildPlaylists::Table)
.if_not_exists()
.col(
ColumnDef::new(GuildPlaylists::GuildId)
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(GuildPlaylists::Name)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(GuildPlaylists::Url)
.string_len(1204)
.not_null(),
)
.primary_key(
Index::create()
.col(GuildPlaylists::GuildId)
.col(GuildPlaylists::Name),
)
.to_owned()
}
fn guild_settings() -> TableCreateStatement {
Table::create()
.table(GuildSettings::Table)
.if_not_exists()
.col(
ColumnDef::new(GuildSettings::GuildId)
.big_integer()
.not_null(),
)
.col(
ColumnDef::new(GuildSettings::Key)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(GuildSettings::Value)
.string_len(1024)
.not_null(),
)
.primary_key(
Index::create()
.col(GuildSettings::GuildId)
.col(GuildSettings::Key),
)
.to_owned()
}
fn media() -> TableCreateStatement {
Table::create()
.table(Media::Table)
.if_not_exists()
.col(
ColumnDef::new(Media::Id)
.big_integer()
.auto_increment()
.not_null()
.primary_key(),
)
.col(ColumnDef::new(Media::Category).string_len(128))
.col(ColumnDef::new(Media::Name).string_len(128))
.col(ColumnDef::new(Media::Url).string_len(128))
.index(
Index::create()
.unique()
.col(Media::Category)
.col(Media::Name),
)
.to_owned()
}
fn statistics() -> TableCreateStatement {
Table::create()
.table(Statistics::Table)
.if_not_exists()
.col(
ColumnDef::new(Statistics::Id)
.big_integer()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(Statistics::Version)
.string_len(32)
.not_null(),
)
.col(
ColumnDef::new(Statistics::Command)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(Statistics::ExecutedAt)
.timestamp()
.not_null(),
)
.col(
ColumnDef::new(Statistics::Success)
.boolean()
.not_null()
.default(true),
)
.col(ColumnDef::new(Statistics::ErrorMsg).string())
.to_owned()
}
fn youtube_songs() -> TableCreateStatement {
Table::create()
.table(YoutubeSongs::Table)
.if_not_exists()
.col(
ColumnDef::new(YoutubeSongs::Id)
.big_integer()
.primary_key()
.auto_increment(),
)
.col(
ColumnDef::new(YoutubeSongs::SpotifyId)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(YoutubeSongs::Artist)
.string_len(128)
.not_null(),
)
.col(
ColumnDef::new(YoutubeSongs::Title)
.string_len(255)
.not_null(),
)
.col(
ColumnDef::new(YoutubeSongs::Album)
.string_len(255)
.not_null(),
)
.col(ColumnDef::new(YoutubeSongs::Url).string_len(128).not_null())
.col(
ColumnDef::new(YoutubeSongs::Score)
.integer()
.default(0)
.not_null(),
)
.index(
Index::create()
.unique()
.col(YoutubeSongs::SpotifyId)
.col(YoutubeSongs::Url),
)
.to_owned()
}

@ -1,7 +0,0 @@
use migration::Migrator;
use sea_orm_migration::prelude::*;
#[tokio::main]
async fn main() {
cli::run_cli(Migrator).await;
}

@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE guild_settings;

@ -0,0 +1,6 @@
CREATE TABLE guild_settings (
guild_id BIGINT NOT NULL,
key VARCHAR(255) NOT NULL,
value VARCHAR(1024),
PRIMARY KEY (guild_id, key)
);

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE guild_playlists;

@ -0,0 +1,7 @@
-- Your SQL goes here
CREATE TABLE guild_playlists (
guild_id BIGINT NOT NULL,
name VARCHAR(255) NOT NULL,
url VARCHAR(1024) NOT NULL,
PRIMARY KEY (guild_id, name)
)

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE gifs;

@ -0,0 +1,8 @@
-- Your SQL goes here
CREATE TABLE gifs (
id BIGSERIAL PRIMARY KEY ,
category VARCHAR(128),
name VARCHAR(128),
url VARCHAR(128) NOT NULL,
UNIQUE (category, name)
)

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE statistics;

@ -0,0 +1,9 @@
-- Your SQL goes here
CREATE TABLE statistics (
id BIGSERIAL PRIMARY KEY,
version VARCHAR(32) NOT NULL,
command VARCHAR(255) NOT NULL,
executed_at TIMESTAMP NOT NULL,
success BOOLEAN NOT NULL DEFAULT TRUE,
error_msg TEXT
)

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE youtube_songs;

@ -0,0 +1,11 @@
-- Your SQL goes here
CREATE TABLE youtube_songs (
id BIGSERIAL PRIMARY KEY,
spotify_id VARCHAR(255) NOT NULL,
artist VARCHAR(128) NOT NULL,
title VARCHAR(255) NOT NULL,
album VARCHAR(255) NOT NULL,
url VARCHAR(128) NOT NULL,
score INTEGER DEFAULT 0 NOT NULL,
UNIQUE (spotify_id, url)
)

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE ephemeral_messages;

@ -0,0 +1,7 @@
-- Your SQL goes here
CREATE TABLE ephemeral_messages (
channel_id BIGINT NOT NULL,
message_id BIGINT NOT NULL,
timeout TIMESTAMP NOT NULL,
PRIMARY KEY (channel_id, message_id)
)

@ -1,49 +1,56 @@
use sea_orm::ActiveValue::Set;
use std::time::SystemTime; use std::time::SystemTime;
use crate::entity::ephemeral_messages; use diesel::prelude::*;
use diesel::{delete, insert_into};
use tokio_diesel::*;
use crate::error::DatabaseResult; use crate::error::DatabaseResult;
use sea_orm::prelude::*; use crate::models::*;
use crate::schema::*;
use crate::Database;
impl super::BotDatabase { impl Database {
/// Adds a command statistic to the database /// Adds a command statistic to the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_ephemeral_message( pub async fn add_ephemeral_message(
&self, &self,
channel_id: u64, channel_id: u64,
message_id: u64, message_id: u64,
timeout: SystemTime, timeout: SystemTime,
) -> DatabaseResult<()> { ) -> DatabaseResult<()> {
let model = ephemeral_messages::ActiveModel { use ephemeral_messages::dsl;
channel_id: Set(channel_id as i64), insert_into(dsl::ephemeral_messages)
message_id: Set(message_id as i64), .values(EphemeralMessageInsert {
timeout: Set(DateTimeLocal::from(timeout).into()), channel_id: channel_id as i64,
..Default::default() message_id: message_id as i64,
}; timeout,
model.insert(&self.db).await?; })
.execute_async(&self.pool)
.await?;
Ok(()) Ok(())
} }
/// Returns a vec of all ephemeral messages /// Returns a vec of all ephemeral messages
#[tracing::instrument(level = "debug", skip(self))] pub async fn get_ephemeral_messages(&self) -> DatabaseResult<Vec<EphemeralMessage>> {
pub async fn get_ephemeral_messages(&self) -> DatabaseResult<Vec<ephemeral_messages::Model>> { use ephemeral_messages::dsl;
let messages = ephemeral_messages::Entity::find().all(&self.db).await?; let messages: Vec<EphemeralMessage> = dsl::ephemeral_messages
.load_async::<EphemeralMessage>(&self.pool)
.await?;
Ok(messages) Ok(messages)
} }
/// Deletes a single ephemeral message /// Deletes a single ephemeral message
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete_ephemeral_message( pub async fn delete_ephemeral_message(
&self, &self,
channel_id: i64, channel_id: i64,
message_id: i64, message_id: i64,
) -> DatabaseResult<()> { ) -> DatabaseResult<()> {
ephemeral_messages::Entity::delete_many() use ephemeral_messages::dsl;
.filter(ephemeral_messages::Column::ChannelId.eq(channel_id)) delete(dsl::ephemeral_messages)
.filter(ephemeral_messages::Column::MessageId.eq(message_id)) .filter(dsl::channel_id.eq(channel_id))
.exec(&self.db) .filter(dsl::message_id.eq(message_id))
.execute_async(&self.pool)
.await?; .await?;
Ok(()) Ok(())

@ -0,0 +1,57 @@
use diesel::insert_into;
use diesel::prelude::*;
use tokio_diesel::*;
use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
impl Database {
/// Returns a list of all gifs in the database
pub async fn get_all_gifs(&self) -> DatabaseResult<Vec<Gif>> {
use gifs::dsl;
log::debug!("Loading all gifs from the database");
let gifs: Vec<Gif> = dsl::gifs.load_async::<Gif>(&self.pool).await?;
Ok(gifs)
}
/// Returns a list of gifs by assigned category
pub async fn get_gifs_by_category(&self, category: &str) -> DatabaseResult<Vec<Gif>> {
use gifs::dsl;
log::debug!("Searching for gifs in category '{}'", category);
let gifs: Vec<Gif> = dsl::gifs
.filter(dsl::category.eq(category))
.load_async::<Gif>(&self.pool)
.await?;
Ok(gifs)
}
/// Adds a gif to the database
pub async fn add_gif(
&self,
url: &str,
category: Option<String>,
name: Option<String>,
) -> DatabaseResult<()> {
use gifs::dsl;
log::debug!(
"Inserting gif with url '{}' and name {:?} and category {:?}",
url,
name,
category
);
insert_into(dsl::gifs)
.values(GifInsert {
url: url.to_string(),
name,
category,
})
.execute_async(&self.pool)
.await?;
Ok(())
}
}

@ -1,54 +1,65 @@
use crate::entity::guild_playlists; use diesel::insert_into;
use diesel::prelude::*;
use tokio_diesel::*;
use crate::error::DatabaseResult; use crate::error::DatabaseResult;
use sea_orm::prelude::*; use crate::models::*;
use sea_orm::ActiveValue::Set; use crate::schema::*;
use crate::Database;
impl super::BotDatabase { impl Database {
/// Returns a list of all guild playlists /// Returns a list of all guild playlists
#[tracing::instrument(level = "debug", skip(self))] pub async fn get_guild_playlists(&self, guild_id: u64) -> DatabaseResult<Vec<GuildPlaylist>> {
pub async fn get_guild_playlists( use guild_playlists::dsl;
&self, log::debug!("Retrieving guild playlists for guild {}", guild_id);
guild_id: u64,
) -> DatabaseResult<Vec<guild_playlists::Model>> { let playlists: Vec<GuildPlaylist> = dsl::guild_playlists
let playlists = guild_playlists::Entity::find() .filter(dsl::guild_id.eq(guild_id as i64))
.filter(guild_playlists::Column::GuildId.eq(guild_id)) .load_async::<GuildPlaylist>(&self.pool)
.all(&self.db)
.await?; .await?;
Ok(playlists) Ok(playlists)
} }
/// Returns a guild playlist by name /// Returns a guild playlist by name
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_guild_playlist( pub async fn get_guild_playlist(
&self, &self,
guild_id: u64, guild_id: u64,
name: String, name: String,
) -> DatabaseResult<Option<guild_playlists::Model>> { ) -> DatabaseResult<Option<GuildPlaylist>> {
let playlist = guild_playlists::Entity::find() use guild_playlists::dsl;
.filter(guild_playlists::Column::GuildId.eq(guild_id)) log::debug!("Retriving guild playlist '{}' for guild {}", name, guild_id);
.filter(guild_playlists::Column::Name.eq(name))
.one(&self.db) let playlists: Vec<GuildPlaylist> = dsl::guild_playlists
.filter(dsl::guild_id.eq(guild_id as i64))
.filter(dsl::name.eq(name))
.load_async::<GuildPlaylist>(&self.pool)
.await?; .await?;
Ok(playlist) Ok(playlists.into_iter().next())
} }
/// Adds a new playlist to the database overwriting the old one /// Adds a new playlist to the database overwriting the old one
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_guild_playlist( pub async fn add_guild_playlist(
&self, &self,
guild_id: u64, guild_id: u64,
name: String, name: String,
url: String, url: String,
) -> DatabaseResult<()> { ) -> DatabaseResult<()> {
let model = guild_playlists::ActiveModel { use guild_playlists::dsl;
guild_id: Set(guild_id as i64), log::debug!("Inserting guild playlist '{}' for guild {}", name, guild_id);
name: Set(name),
url: Set(url), insert_into(dsl::guild_playlists)
..Default::default() .values(GuildPlaylistInsert {
}; guild_id: guild_id as i64,
model.insert(&self.db).await?; name: name.clone(),
url: url.clone(),
})
.on_conflict((dsl::guild_id, dsl::name))
.do_update()
.set(dsl::url.eq(url))
.execute_async(&self.pool)
.await?;
Ok(()) Ok(())
} }

@ -1,46 +1,53 @@
use sea_orm::ActiveValue::Set;
use std::any; use std::any;
use std::fmt::Debug; use std::fmt::Debug;
use std::str::FromStr; use std::str::FromStr;
use crate::entity::guild_settings; use diesel::prelude::*;
use diesel::{delete, insert_into};
use tokio_diesel::*;
use crate::error::DatabaseResult; use crate::error::DatabaseResult;
use sea_orm::prelude::*; use crate::models::*;
use crate::schema::*;
use crate::Database;
impl super::BotDatabase { impl Database {
/// Returns a guild setting from the database /// Returns a guild setting from the database
#[tracing::instrument(level = "debug", skip(self))] pub async fn get_guild_setting<T: 'static>(
pub async fn get_guild_setting<T: 'static, S: AsRef<str> + Debug>(
&self, &self,
guild_id: u64, guild_id: u64,
key: S, key: String,
) -> DatabaseResult<Option<T>> ) -> DatabaseResult<Option<T>>
where where
T: FromStr, T: FromStr,
{ {
let setting = guild_settings::Entity::find() use guild_settings::dsl;
.filter(guild_settings::Column::GuildId.eq(guild_id as i64)) log::debug!("Retrieving setting '{}' for guild {}", key, guild_id);
.filter(guild_settings::Column::Key.eq(key.as_ref()))
.one(&self.db) let entries: Vec<GuildSetting> = dsl::guild_settings
.filter(dsl::guild_id.eq(guild_id as i64))
.filter(dsl::key.eq(key))
.load_async::<GuildSetting>(&self.pool)
.await?; .await?;
if let Some(setting) = setting { log::trace!("Result is {:?}", entries);
if let Some(first) = entries.first() {
if any::TypeId::of::<T>() == any::TypeId::of::<bool>() { if any::TypeId::of::<T>() == any::TypeId::of::<bool>() {
Ok(setting Ok(first
.value .value
.clone() .clone()
.unwrap_or("false".to_string()) .unwrap_or("false".to_string())
.parse::<T>() .parse::<T>()
.ok()) .ok())
} else { } else {
Ok(setting.value.clone().and_then(|v| v.parse::<T>().ok())) Ok(first.value.clone().and_then(|v| v.parse::<T>().ok()))
} }
} else { } else {
Ok(None) return Ok(None);
} }
} }
/// Upserting a guild setting /// Upserting a guild setting
#[tracing::instrument(level = "debug", skip(self))]
pub async fn set_guild_setting<T>( pub async fn set_guild_setting<T>(
&self, &self,
guild_id: u64, guild_id: u64,
@ -48,38 +55,33 @@ impl super::BotDatabase {
value: T, value: T,
) -> DatabaseResult<()> ) -> DatabaseResult<()>
where where
T: 'static + ToString + FromStr + Debug, T: ToString + Debug,
{ {
let model = guild_settings::ActiveModel { use guild_settings::dsl;
guild_id: Set(guild_id as i64), log::debug!("Setting '{}' to '{:?}' for guild {}", key, value, guild_id);
key: Set(key.clone()),
value: Set(Some(value.to_string())), insert_into(dsl::guild_settings)
..Default::default() .values(GuildSettingInsert {
}; guild_id: guild_id as i64,
if self key: key.to_string(),
.get_guild_setting::<T, _>(guild_id, &key) value: value.to_string(),
.await? })
.is_some() .on_conflict((dsl::guild_id, dsl::key))
{ .do_update()
model.update(&self.db).await?; .set(dsl::value.eq(value.to_string()))
} else { .execute_async(&self.pool)
model.insert(&self.db).await?; .await?;
}
Ok(()) Ok(())
} }
/// Deletes a guild setting /// Deletes a guild setting
#[tracing::instrument(level = "debug", skip(self))] pub async fn delete_guild_setting(&self, guild_id: u64, key: String) -> DatabaseResult<()> {
pub async fn delete_guild_setting<S: AsRef<str> + Debug>( use guild_settings::dsl;
&self, delete(dsl::guild_settings)
guild_id: u64, .filter(dsl::guild_id.eq(guild_id as i64))
key: S, .filter(dsl::key.eq(key))
) -> DatabaseResult<()> { .execute_async(&self.pool)
guild_settings::Entity::delete_many()
.filter(guild_settings::Column::GuildId.eq(guild_id))
.filter(guild_settings::Column::Key.eq(key.as_ref()))
.exec(&self.db)
.await?; .await?;
Ok(()) Ok(())

@ -1,48 +0,0 @@
use crate::entity::media;
use crate::error::DatabaseResult;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use std::fmt::Debug;
impl super::BotDatabase {
/// Returns a list of all gifs in the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_all_media(&self) -> DatabaseResult<Vec<media::Model>> {
let entries = media::Entity::find().all(&self.db).await?;
Ok(entries)
}
/// Returns a list of gifs by assigned category
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_media_by_category<S: AsRef<str> + 'static + Debug>(
&self,
category: S,
) -> DatabaseResult<Vec<media::Model>> {
let entries = media::Entity::find()
.filter(media::Column::Category.eq(category.as_ref()))
.all(&self.db)
.await?;
Ok(entries)
}
/// Adds a gif to the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_media(
&self,
url: String,
category: Option<String>,
name: Option<String>,
) -> DatabaseResult<()> {
let model = media::ActiveModel {
url: Set(url),
category: Set(category),
name: Set(name),
..Default::default()
};
model.insert(&self.db).await?;
Ok(())
}
}

@ -1,25 +1,30 @@
pub use ephemeral_messages::*; pub use ephemeral_messages::*;
pub use gifs::*;
pub use guild_playlists::*; pub use guild_playlists::*;
pub use guild_playlists::*; pub use guild_playlists::*;
pub use media::*;
use sea_orm::DatabaseConnection;
pub use statistics::*; pub use statistics::*;
pub use youtube_songs::*; pub use youtube_songs::*;
use crate::PoolConnection;
mod ephemeral_messages; mod ephemeral_messages;
mod gifs;
mod guild_playlists; mod guild_playlists;
mod guild_settings; mod guild_settings;
mod media;
mod statistics; mod statistics;
mod youtube_songs; mod youtube_songs;
#[derive(Clone)] #[derive(Clone)]
pub struct BotDatabase { pub struct Database {
db: DatabaseConnection, pool: PoolConnection,
} }
impl BotDatabase { unsafe impl Send for Database {}
pub fn new(db: DatabaseConnection) -> Self {
Self { db } unsafe impl Sync for Database {}
impl Database {
pub fn new(pool: PoolConnection) -> Self {
Self { pool }
} }
} }

@ -1,49 +1,50 @@
use crate::entity::statistics;
use crate::error::DatabaseResult;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use sea_orm::{FromQueryResult, QuerySelect};
use std::time::SystemTime; use std::time::SystemTime;
#[derive(FromQueryResult)] use diesel::dsl::count;
struct CommandCount { use diesel::insert_into;
count: i64, use diesel::prelude::*;
} use tokio_diesel::*;
impl super::BotDatabase { use crate::error::DatabaseResult;
use crate::models::*;
use crate::schema::*;
use crate::Database;
impl Database {
/// Adds a command statistic to the database /// Adds a command statistic to the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_statistic( pub async fn add_statistic(
&self, &self,
version: String, version: &str,
command: String, command: &str,
executed_at: SystemTime, executed_at: SystemTime,
success: bool, success: bool,
error_msg: Option<String>, error_msg: Option<String>,
) -> DatabaseResult<()> { ) -> DatabaseResult<()> {
let model = statistics::ActiveModel { use statistics::dsl;
version: Set(version), log::trace!("Adding statistic to database");
command: Set(command), insert_into(dsl::statistics)
executed_at: Set(DateTimeLocal::from(executed_at).into()), .values(StatisticInsert {
success: Set(success), version: version.to_string(),
error_msg: Set(error_msg), command: command.to_string(),
..Default::default() executed_at,
}; success,
model.insert(&self.db).await?; error_msg,
})
.execute_async(&self.pool)
.await?;
Ok(()) Ok(())
} }
/// Returns the total number of commands executed /// Returns the total number of commands executed
#[tracing::instrument(level = "debug", skip(self))]
pub async fn get_total_commands_statistic(&self) -> DatabaseResult<u64> { pub async fn get_total_commands_statistic(&self) -> DatabaseResult<u64> {
let total_count: Option<CommandCount> = statistics::Entity::find() use statistics::dsl;
.select_only() log::trace!("Querying total number of commands");
.column_as(statistics::Column::Id.count(), "count") let total_count: i64 = dsl::statistics
.into_model::<CommandCount>() .select(count(dsl::id))
.one(&self.db) .first_async::<i64>(&self.pool)
.await?; .await?;
Ok(total_count.unwrap().count as u64) Ok(total_count as u64)
} }
} }

@ -1,56 +1,69 @@
use crate::entity::youtube_songs; use diesel::prelude::*;
use diesel::{delete, insert_into};
use tokio_diesel::*;
use crate::error::DatabaseResult; use crate::error::DatabaseResult;
use sea_orm::prelude::*; use crate::models::*;
use sea_orm::ActiveValue::Set; use crate::schema::*;
use crate::Database;
impl super::BotDatabase { impl Database {
/// Adds a song to the database or increments the score when it /// Adds a song to the database or increments the score when it
/// already exists /// already exists
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_song( pub async fn add_song(
&self, &self,
spotify_id: String, spotify_id: &str,
artist: String, artist: &str,
title: String, title: &str,
album: String, album: &str,
url: String, url: &str,
) -> DatabaseResult<()> { ) -> DatabaseResult<()> {
if let Some(model) = self.get_song(&spotify_id).await? { use youtube_songs::dsl;
let mut active_model: youtube_songs::ActiveModel = model.into(); log::debug!(
active_model.score = Set(active_model.score.unwrap() + 1); "Inserting/Updating song in database spotify_id: '{}' artist: '{}', title: '{}', album: '{}', url: '{}'",
active_model.update(&self.db).await?; spotify_id,
} else { artist,
let model = youtube_songs::ActiveModel { title,
spotify_id: Set(spotify_id), album,
artist: Set(artist), url,
title: Set(title), );
album: Set(album),
url: Set(url), insert_into(dsl::youtube_songs)
..Default::default() .values(YoutubeSongInsert {
}; spotify_id: spotify_id.to_string(),
model.insert(&self.db).await?; artist: artist.to_string(),
} title: title.to_string(),
album: album.to_string(),
url: url.to_string(),
})
.on_conflict((dsl::spotify_id, dsl::url))
.do_update()
.set(dsl::score.eq(dsl::score + 1))
.execute_async(&self.pool)
.await?;
Ok(()) Ok(())
} }
/// Returns the song with the best score for the given query /// Returns the song with the best score for the given query
#[tracing::instrument(level = "debug", skip(self))] pub async fn get_song(&self, spotify_id: &str) -> DatabaseResult<Option<YoutubeSong>> {
pub async fn get_song(&self, spotify_id: &str) -> DatabaseResult<Option<youtube_songs::Model>> { use youtube_songs::dsl;
let song = youtube_songs::Entity::find() let songs: Vec<YoutubeSong> = dsl::youtube_songs
.filter(youtube_songs::Column::SpotifyId.eq(spotify_id)) .filter(dsl::spotify_id.eq(spotify_id))
.one(&self.db) .order(dsl::score.desc())
.limit(1)
.load_async::<YoutubeSong>(&self.pool)
.await?; .await?;
Ok(song) Ok(songs.into_iter().next())
} }
/// Deletes a song from the database /// Deletes a song from the database
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete_song(&self, id: i64) -> DatabaseResult<()> { pub async fn delete_song(&self, id: i64) -> DatabaseResult<()> {
youtube_songs::Entity::delete_many() use youtube_songs::dsl;
.filter(youtube_songs::Column::Id.eq(id)) delete(dsl::youtube_songs)
.exec(&self.db) .filter(dsl::id.eq(id))
.execute_async(&self.pool)
.await?; .await?;
Ok(()) Ok(())

@ -1,24 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "ephemeral_messages")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub channel_id: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub message_id: i64,
pub timeout: DateTimeWithTimeZone,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -1,24 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "guild_playlists")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub guild_id: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub name: String,
pub url: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -1,24 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "guild_settings")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub guild_id: i64,
#[sea_orm(primary_key, auto_increment = false)]
pub key: String,
pub value: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -1,24 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "media")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub category: Option<String>,
pub name: Option<String>,
pub url: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -1,10 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
pub mod prelude;
pub mod ephemeral_messages;
pub mod guild_playlists;
pub mod guild_settings;
pub mod media;
pub mod statistics;
pub mod youtube_songs;

@ -1,8 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
pub use super::ephemeral_messages::Entity as EphemeralMessages;
pub use super::guild_playlists::Entity as GuildPlaylists;
pub use super::guild_settings::Entity as GuildSettings;
pub use super::media::Entity as Media;
pub use super::statistics::Entity as Statistics;
pub use super::youtube_songs::Entity as YoutubeSongs;

@ -1,27 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "statistics")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub version: String,
pub command: String,
pub executed_at: DateTimeWithTimeZone,
pub success: bool,
#[sea_orm(column_type = "Text", nullable)]
pub error_msg: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -1,27 +0,0 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.7.0
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "youtube_songs")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub spotify_id: String,
pub artist: String,
pub title: String,
pub album: String,
pub url: String,
pub score: i32,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -7,8 +7,20 @@ pub enum DatabaseError {
#[error("DotEnv Error: {0}")] #[error("DotEnv Error: {0}")]
DotEnv(#[from] dotenv::Error), DotEnv(#[from] dotenv::Error),
#[error("{0}")] #[error("Connection Error: {0}")]
SeaOrm(#[from] sea_orm::error::DbErr), ConnectionError(#[from] diesel::prelude::ConnectionError),
#[error("Pool Connection Error: {0}")]
PoolConnectionError(#[from] r2d2::Error),
#[error("Migration Error: {0}")]
MigrationError(#[from] diesel_migrations::RunMigrationsError),
#[error("Result Error: {0}")]
ResultError(#[from] diesel::result::Error),
#[error("AsyncError: {0}")]
AsyncError(#[from] tokio_diesel::AsyncError),
#[error("{0}")] #[error("{0}")]
Msg(String), Msg(String),

@ -1,31 +1,42 @@
#[macro_use]
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
use crate::error::DatabaseResult; use crate::error::DatabaseResult;
use diesel::prelude::*;
use diesel::r2d2::{ConnectionManager, Pool};
use std::env; use std::env;
pub mod database; pub mod database;
pub mod entity;
pub mod error; pub mod error;
pub mod models; pub mod models;
pub mod schema;
pub static VERSION: &str = env!("CARGO_PKG_VERSION"); pub static VERSION: &str = env!("CARGO_PKG_VERSION");
pub use database::BotDatabase as Database; pub use database::Database;
use migration::MigratorTrait;
use sea_orm::{ConnectOptions, Database as SeaDatabase, DatabaseConnection}; type PoolConnection = Pool<ConnectionManager<PgConnection>>;
#[tracing::instrument] embed_migrations!("../bot-database/migrations");
async fn get_connection() -> DatabaseResult<DatabaseConnection> {
fn get_connection() -> DatabaseResult<PoolConnection> {
dotenv::dotenv()?;
let database_url = env::var("DATABASE_URL").expect("No DATABASE_URL in path"); let database_url = env::var("DATABASE_URL").expect("No DATABASE_URL in path");
tracing::debug!("Establishing database connection..."); log::debug!("Establishing database connection...");
let opt = ConnectOptions::new(database_url); let manager = ConnectionManager::<PgConnection>::new(database_url);
let db = SeaDatabase::connect(opt).await?; let pool = Pool::builder().max_size(16).build(manager)?;
tracing::debug!("Running migrations..."); let connection = pool.get()?;
migration::Migrator::up(&db, None).await?; log::debug!("Running migrations...");
tracing::debug!("Migrations finished"); embedded_migrations::run(&connection)?;
tracing::info!("Database connection initialized"); log::debug!("Migrations finished");
log::info!("Database connection initialized");
Ok(db)
Ok(pool)
} }
pub async fn get_database() -> DatabaseResult<Database> { pub fn get_database() -> DatabaseResult<Database> {
let conn = get_connection().await?; let conn = get_connection()?;
Ok(Database::new(conn)) Ok(Database::new(conn))
} }

@ -1,8 +1,94 @@
use super::entity; use crate::schema::*;
use std::time::SystemTime;
pub use entity::ephemeral_messages::Model as EphemeralMessage;
pub use entity::guild_playlists::Model as GuildPlaylist; #[derive(Queryable, Debug)]
pub use entity::guild_settings::Model as GuildSetting; pub struct GuildSetting {
pub use entity::media::Model as Media; pub guild_id: i64,
pub use entity::statistics::Model as Statistic; pub key: String,
pub use entity::youtube_songs::Model as YoutubeSong; pub value: Option<String>,
}
#[derive(Insertable, Debug)]
#[table_name = "guild_settings"]
pub struct GuildSettingInsert {
pub guild_id: i64,
pub key: String,
pub value: String,
}
#[derive(Queryable, Debug)]
pub struct GuildPlaylist {
pub guild_id: i64,
pub name: String,
pub url: String,
}
#[derive(Insertable, Debug)]
#[table_name = "guild_playlists"]
pub struct GuildPlaylistInsert {
pub guild_id: i64,
pub name: String,
pub url: String,
}
#[derive(Queryable, Debug, Clone)]
pub struct Gif {
pub id: i64,
pub category: Option<String>,
pub name: Option<String>,
pub url: String,
}
#[derive(Insertable, Debug)]
#[table_name = "gifs"]
pub struct GifInsert {
pub category: Option<String>,
pub name: Option<String>,
pub url: String,
}
#[derive(Insertable, Debug)]
#[table_name = "statistics"]
pub struct StatisticInsert {
pub version: String,
pub command: String,
pub executed_at: SystemTime,
pub success: bool,
pub error_msg: Option<String>,
}
#[derive(Queryable, Debug, Clone)]
pub struct YoutubeSong {
pub id: i64,
pub spotify_id: String,
pub artist: String,
pub title: String,
pub album: String,
pub url: String,
pub score: i32,
}
#[derive(Insertable, Debug)]
#[table_name = "youtube_songs"]
pub struct YoutubeSongInsert {
pub spotify_id: String,
pub artist: String,
pub title: String,
pub album: String,
pub url: String,
}
#[derive(Queryable, Debug, Clone)]
pub struct EphemeralMessage {
pub channel_id: i64,
pub message_id: i64,
pub timeout: SystemTime,
}
#[derive(Insertable, Debug)]
#[table_name = "ephemeral_messages"]
pub struct EphemeralMessageInsert {
pub channel_id: i64,
pub message_id: i64,
pub timeout: SystemTime,
}

@ -0,0 +1,64 @@
table! {
ephemeral_messages (channel_id, message_id) {
channel_id -> Int8,
message_id -> Int8,
timeout -> Timestamp,
}
}
table! {
gifs (id) {
id -> Int8,
category -> Nullable<Varchar>,
name -> Nullable<Varchar>,
url -> Varchar,
}
}
table! {
guild_playlists (guild_id, name) {
guild_id -> Int8,
name -> Varchar,
url -> Varchar,
}
}
table! {
guild_settings (guild_id, key) {
guild_id -> Int8,
key -> Varchar,
value -> Nullable<Varchar>,
}
}
table! {
statistics (id) {
id -> Int8,
version -> Varchar,
command -> Varchar,
executed_at -> Timestamp,
success -> Bool,
error_msg -> Nullable<Text>,
}
}
table! {
youtube_songs (id) {
id -> Int8,
spotify_id -> Varchar,
artist -> Varchar,
title -> Varchar,
album -> Varchar,
url -> Varchar,
score -> Int4,
}
}
allow_tables_to_appear_in_same_query!(
ephemeral_messages,
gifs,
guild_playlists,
guild_settings,
statistics,
youtube_songs,
);

@ -0,0 +1 @@
target

File diff suppressed because it is too large Load Diff

@ -0,0 +1,15 @@
[package]
name = "bot-serenityutils"
version = "0.2.1"
authors = ["trivernis <trivernis@protonmail.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serenity = "0.10.5"
tokio = "1.4.0"
thiserror = "1.0.24"
log = "0.4.14"
futures = "0.3.14"
serde_json = "1.0.64"

@ -0,0 +1,44 @@
use crate::error::SerenityUtilsResult;
use crate::menu::traits::EventDrivenMessage;
use serenity::http::Http;
use serenity::model::channel::Message;
use serenity::model::id::{ChannelId, MessageId};
use std::sync::Arc;
use std::time::Duration;
pub static SHORT_TIMEOUT: Duration = Duration::from_secs(5);
pub static MEDIUM_TIMEOUT: Duration = Duration::from_secs(20);
pub static LONG_TIMEOUT: Duration = Duration::from_secs(60);
pub static EXTRA_LONG_TIMEOUT: Duration = Duration::from_secs(600);
pub type BoxedEventDrivenMessage = Box<dyn EventDrivenMessage>;
#[derive(Copy, Clone, Debug, PartialOrd, PartialEq, Eq, Hash)]
pub struct MessageHandle {
pub channel_id: u64,
pub message_id: u64,
}
impl MessageHandle {
/// Creates a new message handle
pub fn new(channel_id: ChannelId, message_id: MessageId) -> Self {
Self {
message_id: message_id.0,
channel_id: channel_id.0,
}
}
/// Creates a new message handle from raw ids
pub fn from_raw_ids(channel_id: u64, message_id: u64) -> Self {
Self {
message_id,
channel_id,
}
}
/// Returns the message object of the handle
pub async fn get_message(&self, http: &Arc<Http>) -> SerenityUtilsResult<Message> {
let msg = http.get_message(self.channel_id, self.message_id).await?;
Ok(msg)
}
}

@ -0,0 +1,56 @@
use crate::core::MessageHandle;
use crate::error::SerenityUtilsResult;
use serenity::builder::CreateMessage;
use serenity::http::Http;
use serenity::model::channel::Message;
use serenity::model::id::ChannelId;
use std::sync::Arc;
use std::time::Duration;
pub struct EphemeralMessage;
impl EphemeralMessage {
/// Ensures that an already existing message is
/// deleted after a certain amount of time
pub async fn create_from_message(
http: &Arc<Http>,
message: &Message,
timeout: Duration,
) -> SerenityUtilsResult<()> {
log::debug!("Creating ephemeral message from existing message");
let handle = MessageHandle::new(message.channel_id, message.id);
let http = Arc::clone(&http);
log::debug!("Starting delete task");
tokio::spawn(async move {
log::debug!("Waiting for timeout to pass");
tokio::time::sleep(timeout).await;
log::debug!("Deleting ephemeral message");
if let Err(e) = http
.delete_message(handle.channel_id, handle.message_id)
.await
{
log::error!("Failed to delete ephemeral message {:?}: {}", handle, e);
}
});
Ok(())
}
/// Creates a new message that is deleted after a certain amount of time
pub async fn create<'a, F>(
http: &Arc<Http>,
channel_id: ChannelId,
timeout: Duration,
f: F,
) -> SerenityUtilsResult<Message>
where
F: for<'b> FnOnce(&'b mut CreateMessage<'a>) -> &'b mut CreateMessage<'a>,
{
log::debug!("Creating new ephemeral message");
let msg = channel_id.send_message(http, f).await?;
Self::create_from_message(http, &msg, timeout).await?;
Ok(msg)
}
}

@ -0,0 +1,18 @@
use thiserror::Error;
pub type SerenityUtilsResult<T> = Result<T, SerenityUtilsError>;
#[derive(Debug, Error)]
pub enum SerenityUtilsError {
#[error("Serenity Error: {0}")]
SerenityError(#[from] serenity::Error),
#[error("Page {0} not found")]
PageNotFound(usize),
#[error("Serenity Utils not fully initialized")]
Uninitialized,
#[error("{0}")]
Msg(String),
}

@ -0,0 +1,7 @@
pub mod core;
pub mod ephemeral_message;
pub mod error;
pub mod macros;
pub mod menu;
pub static VERSION: &str = env!("CARGO_PKG_VERSION");

@ -0,0 +1,16 @@
/// Forwards the error directly to the user
/// without having to accept it in any handler.
/// Can only be used in async functions that return a Result.
#[macro_export]
macro_rules! forward_error {
($ctx:expr,$channel_id:expr,$result:expr) => {
match $result {
Err(e) => {
use bot_serenityutils::{core::SHORT_TIMEOUT, ephemeral_message::EphemeralMessage};
$channel_id.say($ctx, format!("‼️ {}", e)).await?;
return Ok(());
}
Ok(v) => v,
}
};
}

@ -0,0 +1,181 @@
use crate::core::{BoxedEventDrivenMessage, MessageHandle};
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
use serenity::client::Context;
use serenity::model::prelude::*;
use serenity::prelude::TypeMapKey;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
/// Container to store event driven messages in the serenity context data
pub struct EventDrivenMessageContainer;
pub type MessageRef = Arc<Mutex<BoxedEventDrivenMessage>>;
pub type EventDrivenMessagesRef = Arc<Mutex<HashMap<MessageHandle, MessageRef>>>;
impl TypeMapKey for EventDrivenMessageContainer {
type Value = EventDrivenMessagesRef;
}
static UPDATE_INTERVAL_SECS: u64 = 5;
/// Starts the loop to handle message updates
pub async fn start_update_loop(ctx: &Context) {
let event_messages = get_listeners_from_context(ctx)
.await
.expect("Failed to get event message container");
let http = Arc::clone(&ctx.http);
tokio::task::spawn(async move {
loop {
{
log::trace!("Locking listener from update loop.");
let messages = {
let msgs_lock = event_messages.lock().await;
msgs_lock
.iter()
.map(|(k, v)| (*k, v.clone()))
.collect::<Vec<(MessageHandle, MessageRef)>>()
};
log::trace!("Listener locked.");
let mut frozen_messages = Vec::new();
for (key, msg) in messages {
let mut msg = msg.lock().await;
if let Err(e) = msg.update(&http).await {
log::error!("Failed to update message: {:?}", e);
}
if msg.is_frozen() {
frozen_messages.push(key);
}
}
{
let mut msgs_lock = event_messages.lock().await;
for key in frozen_messages {
msgs_lock.remove(&key);
}
}
}
log::trace!("Listener unlocked");
tokio::time::sleep(Duration::from_secs(UPDATE_INTERVAL_SECS)).await;
}
});
}
/// To be fired from the serenity handler when a message was deleted
pub async fn handle_message_delete(
ctx: &Context,
channel_id: ChannelId,
message_id: MessageId,
) -> SerenityUtilsResult<()> {
let mut affected_messages = Vec::new();
{
let listeners = get_listeners_from_context(ctx).await?;
log::trace!("Locking listener from handle_message_delete.");
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
let handle = MessageHandle::new(channel_id, message_id);
if let Some(msg) = listeners_lock.get(&handle) {
affected_messages.push(Arc::clone(msg));
listeners_lock.remove(&handle);
}
}
log::trace!("Listener unlocked");
for msg in affected_messages {
let mut msg = msg.lock().await;
msg.on_deleted(ctx).await?;
}
Ok(())
}
/// To be fired from the serenity handler when multiple messages were deleted
pub async fn handle_message_delete_bulk(
ctx: &Context,
channel_id: ChannelId,
message_ids: &Vec<MessageId>,
) -> SerenityUtilsResult<()> {
let mut affected_messages = Vec::new();
{
let listeners = get_listeners_from_context(ctx).await?;
log::trace!("Locking listener from handle_message_delete_bulk.");
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
for message_id in message_ids {
let handle = MessageHandle::new(channel_id, *message_id);
if let Some(msg) = listeners_lock.get_mut(&handle) {
affected_messages.push(Arc::clone(msg));
listeners_lock.remove(&handle);
}
}
}
log::trace!("Listener unlocked");
for msg in affected_messages {
let mut msg = msg.lock().await;
msg.on_deleted(ctx).await?;
}
Ok(())
}
/// Fired when a reaction was added to a message
pub async fn handle_reaction_add(ctx: &Context, reaction: &Reaction) -> SerenityUtilsResult<()> {
let mut affected_messages = Vec::new();
{
let listeners = get_listeners_from_context(ctx).await?;
log::trace!("Locking listener from handle_reaction_add.");
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
let handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
if let Some(msg) = listeners_lock.get_mut(&handle) {
affected_messages.push(Arc::clone(&msg));
}
}
log::trace!("Listener unlocked");
for msg in affected_messages {
let mut msg = msg.lock().await;
msg.on_reaction_add(ctx, reaction.clone()).await?;
}
Ok(())
}
/// Fired when a reaction was added to a message
pub async fn handle_reaction_remove(ctx: &Context, reaction: &Reaction) -> SerenityUtilsResult<()> {
let mut affected_messages = Vec::new();
{
let listeners = get_listeners_from_context(ctx).await?;
log::trace!("Locking listener from handle_reaction_remove.");
let mut listeners_lock = listeners.lock().await;
log::trace!("Listener locked.");
let handle = MessageHandle::new(reaction.channel_id, reaction.message_id);
if let Some(msg) = listeners_lock.get_mut(&handle) {
affected_messages.push(Arc::clone(&msg));
}
}
log::trace!("Listener unlocked");
for msg in affected_messages {
let mut msg = msg.lock().await;
msg.on_reaction_remove(ctx, reaction.clone()).await?;
}
Ok(())
}
pub async fn get_listeners_from_context(
ctx: &Context,
) -> SerenityUtilsResult<EventDrivenMessagesRef> {
let data = ctx.data.read().await;
let listeners = data
.get::<EventDrivenMessageContainer>()
.ok_or(SerenityUtilsError::Uninitialized)?;
log::trace!("Returning listener");
Ok(listeners.clone())
}

@ -0,0 +1,69 @@
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
use crate::menu::container::get_listeners_from_context;
use crate::menu::menu::Menu;
use serenity::client::Context;
use serenity::http::CacheHttp;
use serenity::model::channel::Reaction;
/// Shows the next page in the menu
pub async fn next_page(ctx: &Context, menu: &mut Menu<'_>, _: Reaction) -> SerenityUtilsResult<()> {
log::debug!("Showing next page");
menu.current_page = (menu.current_page + 1) % menu.pages.len();
display_page(ctx, menu).await?;
Ok(())
}
/// Shows the previous page in the menu
pub async fn previous_page(
ctx: &Context,
menu: &mut Menu<'_>,
_: Reaction,
) -> SerenityUtilsResult<()> {
log::debug!("Showing previous page");
if menu.current_page == 0 {
menu.current_page = menu.pages.len() - 1;
} else {
menu.current_page = menu.current_page - 1;
}
display_page(ctx, menu).await?;
Ok(())
}
/// Shows the previous page in the menu
pub async fn close_menu(
ctx: &Context,
menu: &mut Menu<'_>,
_: Reaction,
) -> SerenityUtilsResult<()> {
log::debug!("Closing menu");
menu.close(ctx.http()).await?;
let listeners = get_listeners_from_context(&ctx).await?;
let mut listeners_lock = listeners.lock().await;
let message = menu.message.read().await;
listeners_lock.remove(&*message);
Ok(())
}
/// Displays the menu page
async fn display_page(ctx: &Context, menu: &mut Menu<'_>) -> SerenityUtilsResult<()> {
log::debug!("Displaying page {}", menu.current_page);
let page = menu
.pages
.get(menu.current_page)
.ok_or(SerenityUtilsError::PageNotFound(menu.current_page))?
.get()
.await?;
let mut msg = menu.get_message(ctx.http()).await?;
msg.edit(ctx, |e| {
e.0.clone_from(&mut page.0.clone());
e
})
.await?;
log::debug!("Page displayed");
Ok(())
}

@ -0,0 +1,393 @@
use crate::core::MessageHandle;
use crate::error::{SerenityUtilsError, SerenityUtilsResult};
use crate::menu::container::get_listeners_from_context;
use crate::menu::controls::{close_menu, next_page, previous_page};
use crate::menu::traits::EventDrivenMessage;
use crate::menu::{EventDrivenMessagesRef, Page};
use futures::FutureExt;
use serenity::async_trait;
use serenity::client::Context;
use serenity::http::Http;
use serenity::model::channel::{Message, Reaction, ReactionType};
use serenity::model::id::ChannelId;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::{Mutex, RwLock};
pub static NEXT_PAGE_EMOJI: &str = "➡️";
pub static PREVIOUS_PAGE_EMOJI: &str = "⬅️";
pub static CLOSE_MENU_EMOJI: &str = "❌";
pub type ControlActionResult<'b> =
Pin<Box<dyn Future<Output = SerenityUtilsResult<()>> + Send + 'b>>;
pub type ControlActionArc = Arc<
dyn for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
+ Send
+ Sync,
>;
#[derive(Clone)]
pub struct ActionContainer {
inner: ControlActionArc,
position: usize,
}
impl ActionContainer {
/// Creates a new control action
pub fn new<F: 'static>(position: usize, callback: F) -> Self
where
F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
+ Send
+ Sync,
{
Self {
inner: Arc::new(callback),
position,
}
}
/// Runs the action
pub async fn run(
&self,
ctx: &Context,
menu: &mut Menu<'_>,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
self.inner.clone()(ctx, menu, reaction).await?;
Ok(())
}
}
/// A menu message
#[derive(Clone)]
pub struct Menu<'a> {
pub message: Arc<RwLock<MessageHandle>>,
pub pages: Vec<Page<'a>>,
pub current_page: usize,
pub controls: HashMap<String, ActionContainer>,
pub timeout: Instant,
pub sticky: bool,
closed: bool,
listeners: EventDrivenMessagesRef,
}
impl Menu<'_> {
/// Removes all reactions from the menu
pub(crate) async fn close(&mut self, http: &Http) -> SerenityUtilsResult<()> {
log::debug!("Closing menu...");
let handle = self.message.read().await;
http.delete_message_reactions(handle.channel_id, handle.message_id)
.await?;
self.closed = true;
Ok(())
}
/// Returns the message of the menu
pub async fn get_message(&self, http: &Http) -> SerenityUtilsResult<Message> {
let handle = self.message.read().await;
let msg = http
.get_message(handle.channel_id, handle.message_id)
.await?;
Ok(msg)
}
/// Recreates the message completely
pub async fn recreate(&self, http: &Http) -> SerenityUtilsResult<()> {
log::debug!("Recreating message");
let old_handle = {
let handle = self.message.read().await;
(*handle).clone()
};
log::debug!("Getting current page");
let current_page = self
.pages
.get(self.current_page)
.cloned()
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?
.get()
.await?;
log::debug!("Creating new message");
let message = http
.send_message(
old_handle.channel_id,
&serde_json::to_value(current_page.0).unwrap(),
)
.await?;
let mut controls = self
.controls
.clone()
.into_iter()
.collect::<Vec<(String, ActionContainer)>>();
controls.sort_by_key(|(_, a)| a.position);
for emoji in controls.into_iter().map(|(e, _)| e) {
http.create_reaction(
message.channel_id.0,
message.id.0,
&ReactionType::Unicode(emoji.clone()),
)
.await?;
}
log::trace!("New message is {:?}", message);
let new_handle = {
let mut handle = self.message.write().await;
handle.message_id = message.id.0;
(*handle).clone()
};
{
log::debug!("Changing key of message");
let mut listeners_lock = self.listeners.lock().await;
let menu = listeners_lock.remove(&old_handle).unwrap();
listeners_lock.insert(new_handle, menu);
}
log::debug!("Deleting original message");
http.delete_message(old_handle.channel_id, old_handle.message_id)
.await?;
log::debug!("Message recreated");
Ok(())
}
}
#[async_trait]
impl<'a> EventDrivenMessage for Menu<'a> {
fn is_frozen(&self) -> bool {
self.closed
}
async fn update(&mut self, http: &Http) -> SerenityUtilsResult<()> {
log::trace!("Checking for menu timeout");
if Instant::now() >= self.timeout {
log::debug!("Menu timout reached. Closing menu.");
self.close(http).await?;
} else if self.sticky {
log::debug!("Message is sticky. Checking for new messages in channel...");
let handle = {
let handle = self.message.read().await;
(*handle).clone()
};
let channel_id = ChannelId(handle.channel_id);
let messages = channel_id
.messages(http, |p| p.after(handle.message_id).limit(1))
.await?;
log::trace!("Messages are {:?}", messages);
if messages.len() > 0 {
log::debug!("New messages in channel. Recreating...");
self.recreate(http).await?;
}
}
Ok(())
}
async fn on_reaction_add(
&mut self,
ctx: &Context,
reaction: Reaction,
) -> SerenityUtilsResult<()> {
log::debug!("Reaction to menu added");
let current_user = ctx.http.get_current_user().await?;
if reaction.user_id.unwrap().0 == current_user.id.0 {
log::debug!("Reaction is from current user.");
return Ok(());
}
let emoji_string = reaction.emoji.as_data();
log::debug!("Deleting user reaction.");
reaction.delete(ctx).await?;
if let Some(control) = self.controls.get(&emoji_string).cloned() {
log::debug!("Running control");
control.run(ctx, self, reaction).await?;
}
Ok(())
}
}
/// A builder for messages
pub struct MenuBuilder {
pages: Vec<Page<'static>>,
current_page: usize,
controls: HashMap<String, ActionContainer>,
timeout: Duration,
sticky: bool,
}
impl Default for MenuBuilder {
fn default() -> Self {
Self {
pages: vec![],
current_page: 0,
controls: HashMap::new(),
timeout: Duration::from_secs(60),
sticky: false,
}
}
}
impl MenuBuilder {
/// Creates a new paginaton menu
pub fn new_paginator() -> Self {
log::debug!("Creating new paginator");
let mut controls = HashMap::new();
controls.insert(
PREVIOUS_PAGE_EMOJI.to_string(),
ActionContainer::new(0, |c, m, r| previous_page(c, m, r).boxed()),
);
controls.insert(
CLOSE_MENU_EMOJI.to_string(),
ActionContainer::new(1, |c, m, r| close_menu(c, m, r).boxed()),
);
controls.insert(
NEXT_PAGE_EMOJI.to_string(),
ActionContainer::new(2, |c, m, r| next_page(c, m, r).boxed()),
);
Self {
controls,
..Default::default()
}
}
/// Adds a page to the message builder
pub fn add_page(mut self, page: Page<'static>) -> Self {
self.pages.push(page);
self
}
/// Adds multiple pages to the message
pub fn add_pages<I>(mut self, pages: I) -> Self
where
I: IntoIterator<Item = Page<'static>>,
{
let mut pages = pages.into_iter().collect();
self.pages.append(&mut pages);
self
}
/// Adds a single control to the message
pub fn add_control<S, F: 'static>(mut self, position: usize, emoji: S, action: F) -> Self
where
S: ToString,
F: for<'b> Fn(&'b Context, &'b mut Menu<'_>, Reaction) -> ControlActionResult<'b>
+ Send
+ Sync,
{
self.controls
.insert(emoji.to_string(), ActionContainer::new(position, action));
self
}
/// Adds a single control to the message
pub fn add_controls<S, I>(mut self, controls: I) -> Self
where
S: ToString,
I: IntoIterator<Item = (usize, S, ControlActionArc)>,
{
for (position, emoji, action) in controls {
self.controls.insert(
emoji.to_string(),
ActionContainer {
position,
inner: action,
},
);
}
self
}
/// Sets the timeout for the message
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Sets the start page of the message
pub fn start_page(mut self, page: usize) -> Self {
self.current_page = page;
self
}
/// If the message should be sticky and always be
/// the last one in the channel
pub fn sticky(mut self, value: bool) -> Self {
self.sticky = value;
self
}
/// builds the menu
pub async fn build(
self,
ctx: &Context,
channel_id: ChannelId,
) -> SerenityUtilsResult<Arc<RwLock<MessageHandle>>> {
log::debug!("Building menu...");
let mut current_page = self
.pages
.get(self.current_page)
.ok_or(SerenityUtilsError::PageNotFound(self.current_page))?
.clone()
.get()
.await?;
let message = channel_id.send_message(ctx, |_| &mut current_page).await?;
log::trace!("Message is {:?}", message);
let listeners = get_listeners_from_context(ctx).await?;
log::debug!("Sorting controls...");
let mut controls = self
.controls
.clone()
.into_iter()
.collect::<Vec<(String, ActionContainer)>>();
controls.sort_by_key(|(_, a)| a.position);
log::debug!("Creating menu...");
let message_handle = MessageHandle::new(message.channel_id, message.id);
let handle_lock = Arc::new(RwLock::new(message_handle));
let menu = Menu {
message: Arc::clone(&handle_lock),
pages: self.pages,
current_page: self.current_page,
controls: self.controls,
timeout: Instant::now() + self.timeout,
closed: false,
listeners: Arc::clone(&listeners),
sticky: self.sticky,
};
log::debug!("Storing menu to listeners...");
{
let mut listeners_lock = listeners.lock().await;
log::trace!("Listeners locked.");
listeners_lock.insert(message_handle, Arc::new(Mutex::new(Box::new(menu))));
}
log::debug!("Adding controls...");
for (emoji, _) in controls {
message
.react(ctx, ReactionType::Unicode(emoji.clone()))
.await?;
}
log::debug!("Menu successfully created.");
Ok(handle_lock)
}
}

@ -0,0 +1,15 @@
pub(crate) mod container;
pub(crate) mod controls;
pub(crate) mod menu;
pub(crate) mod page;
pub(crate) mod traits;
pub use container::*;
pub use controls::*;
pub use menu::{
ActionContainer, ControlActionArc, Menu, MenuBuilder, CLOSE_MENU_EMOJI, NEXT_PAGE_EMOJI,
PREVIOUS_PAGE_EMOJI,
};
pub use page::*;
pub use traits::EventDrivenMessage;

@ -0,0 +1,40 @@
use crate::error::SerenityUtilsResult;
use serenity::builder::CreateMessage;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
pub type MessageBuildOutput<'b> =
Pin<Box<dyn Future<Output = SerenityUtilsResult<CreateMessage<'b>>> + Send + 'b>>;
pub type MessageBuilderFn<'b> = Arc<dyn Fn() -> MessageBuildOutput<'b> + Send + Sync>;
#[derive(Clone)]
/// A page that stores a builder function for message pages
/// or static pages
pub enum Page<'b> {
Builder(MessageBuilderFn<'b>),
Static(CreateMessage<'b>),
}
impl<'b> Page<'b> {
/// Creates a new page with the given builder function
pub fn new_builder<F: 'static>(builder_fn: F) -> Self
where
F: Fn() -> MessageBuildOutput<'b> + Send + Sync,
{
Self::Builder(Arc::new(builder_fn))
}
/// Creates a new page with a static message
pub fn new_static(page: CreateMessage<'b>) -> Self {
Self::Static(page)
}
/// Returns the CreateMessage of the page
pub async fn get(&self) -> SerenityUtilsResult<CreateMessage<'b>> {
match self {
Page::Builder(b) => b().await,
Page::Static(inner) => Ok(inner.clone()),
}
}
}

@ -0,0 +1,40 @@
use crate::error::SerenityUtilsResult;
use serenity::client::Context;
use serenity::http::Http;
use serenity::{async_trait, model::prelude::*};
#[async_trait]
pub trait EventDrivenMessage: Send + Sync {
/// Returns if a message has been frozen and won't handle any further events
fn is_frozen(&self) -> bool {
false
}
/// Fired periodically
async fn update(&mut self, _http: &Http) -> SerenityUtilsResult<()> {
Ok(())
}
/// Fired when the message was deleted
async fn on_deleted(&mut self, _ctx: &Context) -> SerenityUtilsResult<()> {
Ok(())
}
/// Fired when a reaction was added to the message
async fn on_reaction_add(
&mut self,
_ctx: &Context,
_reaction: Reaction,
) -> SerenityUtilsResult<()> {
Ok(())
}
/// Fired when a reaction was removed from the message
async fn on_reaction_remove(
&mut self,
_ctx: &Context,
_reaction: Reaction,
) -> SerenityUtilsResult<()> {
Ok(())
}
}

@ -1,39 +1,40 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::env;
use std::time::SystemTime;
use bot_database::get_database; use bot_database::get_database;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::buckets::LimitedFor;
use serenity::framework::standard::macros::hook; use serenity::framework::standard::macros::hook;
use serenity::framework::standard::{CommandResult, DispatchError}; use serenity::framework::standard::{CommandResult, DispatchError};
use serenity::framework::StandardFramework; use serenity::framework::StandardFramework;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::model::id::UserId; use serenity::model::id::UserId;
use serenity::prelude::GatewayIntents;
use serenity::Client; use serenity::Client;
use serenity_additions::RegisterAdditions;
use songbird::SerenityInit; use songbird::SerenityInit;
use crate::commands::*; use crate::commands::*;
use crate::handler::{get_raw_event_handler, Handler}; use crate::handler::Handler;
use crate::utils::context_data::{ use crate::utils::context_data::{get_database_from_context, DatabaseContainer, Store, StoreData};
get_database_from_context, DatabaseContainer, MusicPlayers, Store, StoreData,
};
use crate::utils::error::{BotError, BotResult}; use crate::utils::error::{BotError, BotResult};
use bot_serenityutils::menu::EventDrivenMessageContainer;
use serenity::framework::standard::buckets::LimitedFor;
use std::sync::Arc;
use std::time::SystemTime;
use tokio::sync::Mutex;
pub async fn get_client() -> BotResult<Client> { pub async fn get_client() -> BotResult<Client> {
let token = env::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?; let token = dotenv::var("BOT_TOKEN").map_err(|_| BotError::MissingToken)?;
let database = get_database().await?; let database = get_database()?;
let client = Client::builder(token, GatewayIntents::all())
.register_serenity_additions_with(get_raw_event_handler()) let client = Client::builder(token)
.event_handler(Handler) .event_handler(Handler)
.framework(get_framework().await) .framework(get_framework().await)
.register_songbird() .register_songbird()
.type_map_insert::<Store>(StoreData::create().await)
.type_map_insert::<DatabaseContainer>(database)
.type_map_insert::<MusicPlayers>(HashMap::new())
.await?; .await?;
{
let mut data = client.data.write().await;
data.insert::<Store>(StoreData::new());
data.insert::<DatabaseContainer>(database);
data.insert::<EventDrivenMessageContainer>(Arc::new(Mutex::new(HashMap::new())));
}
Ok(client) Ok(client)
} }
@ -93,13 +94,13 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
m.embed(|e| e.title("Error occurred").description(format!("{}", why))) m.embed(|e| e.title("Error occurred").description(format!("{}", why)))
}) })
.await; .await;
tracing::warn!("Error in {}: {:?}", cmd_name, why); log::warn!("Error in {}: {:?}", cmd_name, why);
} }
let database = get_database_from_context(ctx).await; let database = get_database_from_context(ctx).await;
let _ = database let _ = database
.add_statistic( .add_statistic(
crate::VERSION.to_string(), crate::VERSION,
cmd_name.to_string(), cmd_name,
SystemTime::now(), SystemTime::now(),
error_msg.is_none(), error_msg.is_none(),
error_msg, error_msg,
@ -109,13 +110,13 @@ async fn after_hook(ctx: &Context, msg: &Message, cmd_name: &str, error: Command
#[hook] #[hook]
async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool { async fn before_hook(ctx: &Context, msg: &Message, _: &str) -> bool {
tracing::trace!("Got command message {}", msg.content); log::trace!("Got command message {}", msg.content);
let _ = msg.channel_id.broadcast_typing(ctx).await; let _ = msg.channel_id.broadcast_typing(ctx).await;
true true
} }
#[hook] #[hook]
async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, command_name: &str) { async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError) {
match error { match error {
DispatchError::Ratelimited(info) => { DispatchError::Ratelimited(info) => {
if info.is_first_try { if info.is_first_try {
@ -131,19 +132,13 @@ async fn dispatch_error(ctx: &Context, msg: &Message, error: DispatchError, comm
DispatchError::OnlyForDM => { DispatchError::OnlyForDM => {
let _ = msg let _ = msg
.channel_id .channel_id
.say( .say(&ctx.http, "This command only works via DM")
&ctx.http,
format!("The command {command_name} only works via DM"),
)
.await; .await;
} }
DispatchError::OnlyForGuilds => { DispatchError::OnlyForGuilds => {
let _ = msg let _ = msg
.channel_id .channel_id
.say( .say(&ctx.http, "This command only works on servers")
&ctx.http,
format!("The command {command_name} only works on servers"),
)
.await; .await;
} }
DispatchError::NotEnoughArguments { min, given } => { DispatchError::NotEnoughArguments { min, given } => {

@ -16,7 +16,7 @@ pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> Com
let data = ctx.data.read().await; let data = ctx.data.read().await;
let store = data.get::<Store>().expect("Failed to get store"); let store = data.get::<Store>().expect("Failed to get store");
let enchantment_name = args.message().to_lowercase(); let enchantment_name = args.message().to_lowercase();
tracing::debug!("Searching for enchantment {}", enchantment_name); log::debug!("Searching for enchantment {}", enchantment_name);
let enchantments_by_name = store let enchantments_by_name = store
.minecraft_data_api .minecraft_data_api
@ -29,7 +29,7 @@ pub(crate) async fn enchantment(ctx: &Context, msg: &Message, args: Args) -> Com
enchantment_name enchantment_name
)))? )))?
.clone(); .clone();
tracing::trace!("Enchantment is {:?}", enchantment); log::trace!("Enchantment is {:?}", enchantment);
msg.channel_id msg.channel_id
.send_message(ctx, |m| { .send_message(ctx, |m| {

@ -1,10 +1,8 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::{macros::command, Args, CommandResult}; use serenity::framework::standard::{macros::command, Args, CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::messages::minecraft::item::create_item_message;
use crate::providers::minecraft::get_item_full_information;
use crate::utils::context_data::Store; use crate::utils::context_data::Store;
#[command] #[command]
@ -19,11 +17,51 @@ pub(crate) async fn item(ctx: &Context, msg: &Message, args: Args) -> CommandRes
let store = data.get::<Store>().expect("Failed to get store"); let store = data.get::<Store>().expect("Failed to get store");
let item_name = args.message().to_lowercase(); let item_name = args.message().to_lowercase();
tracing::debug!("Searching for item '{}'", item_name); log::debug!("Searching for item '{}'", item_name);
let information = get_item_full_information(&item_name, &store.minecraft_data_api)?; let items_by_name = store.minecraft_data_api.items.items_by_name()?;
tracing::trace!("Item full information is {:?}", information); let item = items_by_name
create_item_message(ctx, msg.channel_id, information).await?; .get(&item_name)
.ok_or(CommandError::from(format!(
"The item `{}` could not be found",
item_name
)))?;
let enchantments_by_category = store
.minecraft_data_api
.enchantments
.enchantments_by_category()?;
log::trace!("Item is {:?}", item);
msg.channel_id
.send_message(ctx, |m| {
m.embed(|mut e| {
e = e
.title(&*item.display_name)
.thumbnail(format!(
"https://minecraftitemids.com/item/128/{}.png",
item.name
))
.field("Name", &*item.name, false)
.field("Stack Size", item.stack_size, false);
if let Some(durability) = item.durability {
e = e.field("Durability", durability, true);
}
if let Some(variations) = &item.variations {
e = e.field("Variations", format!("{:?}", variations), false);
}
if let Some(enchant_categories) = &item.enchant_categories {
let item_enchantments = enchant_categories
.into_iter()
.filter_map(|c| enchantments_by_category.get(c))
.flatten()
.map(|e| e.display_name.clone())
.collect::<Vec<String>>();
e = e.field("Enchantments", item_enchantments.join(", "), false);
}
e
})
})
.await?;
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;
Ok(()) Ok(())

@ -1,21 +1,21 @@
use crate::utils::context_data::get_database_from_context; use crate::utils::context_data::get_database_from_context;
use bot_coreutils::url; use bot_coreutils::url;
use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Adds media to the database")] #[description("Simple ping test command")]
#[usage("<url> [<category>] [<name>]")] #[usage("<url> [<category>] [<name>]")]
#[bucket("general")] #[bucket("general")]
#[aliases("add_gif", "add-gif", "addgif", "add-media", "addmedia")] #[aliases("add-gif", "addgif")]
#[min_args(1)] #[min_args(1)]
#[max_args(3)] #[max_args(3)]
#[owners_only] #[owners_only]
async fn add_media(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn add_gif(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let url = args.single::<String>()?; let url = args.single::<String>()?;
if !url::is_valid(&url) { if !url::is_valid(&url) {
@ -26,10 +26,10 @@ async fn add_media(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
let name = args.single_quoted::<String>().ok(); let name = args.single_quoted::<String>().ok();
let database = get_database_from_context(&ctx).await; let database = get_database_from_context(&ctx).await;
database.add_media(url, category, name).await?; database.add_gif(&url, category, name).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |c| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |c| {
c.reference_message(msg) c.reference_message(msg)
.content("Media entry added to the database.") .content("Gif added to the database.")
}) })
.await?; .await?;

@ -1,39 +0,0 @@
use futures::future::BoxFuture;
use futures::FutureExt;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use serenity::Result as SerenityResult;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command]
#[description("Clears the chat (maximum 100 messages)")]
#[usage("[<number>]")]
#[example("20")]
#[min_args(0)]
#[max_args(1)]
#[bucket("general")]
#[required_permissions("MANAGE_MESSAGES")]
async fn clear(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let limit = args.single::<u64>().unwrap_or(20);
tracing::debug!("Deleting messages for channel {}", msg.channel_id);
let messages = msg.channel_id.messages(ctx, |b| b.limit(limit)).await?;
tracing::debug!("Deleting {} messages", messages.len());
let futures: Vec<BoxFuture<SerenityResult<()>>> = messages
.into_iter()
.map(|m| async move { ctx.http.delete_message(m.channel_id.0, m.id.0).await }.boxed())
.collect();
tracing::debug!("Waiting for all messages to be deleted");
let deleted = futures::future::join_all(futures).await;
let deleted_count = deleted.into_iter().filter(|d| d.is_ok()).count();
tracing::debug!("{} Messages deleted", deleted_count);
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |f| {
f.content(format!("Deleted {} messages", deleted_count))
})
.await?;
Ok(())
}

@ -1,44 +0,0 @@
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use std::time::Duration;
#[command]
#[description("Fuck this person in particular")]
#[usage("<person> [<amount>] [<verbosity>]")]
#[min_args(1)]
#[max_args(3)]
#[bucket("general")]
#[aliases("frick", "fock")]
async fn fuck(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut amount = args.single::<usize>().unwrap_or(3);
if amount > 3 {
msg.reply(&ctx.http, "Don't you think that's a bit much?")
.await?;
tokio::time::sleep(Duration::from_secs(2)).await;
amount = 3;
} else {
msg.reply(&ctx.http, "no").await?;
tokio::time::sleep(Duration::from_secs(1)).await;
}
let mut verbosity = args.single::<usize>().unwrap_or(1);
if verbosity == 0 {
verbosity = 1
}
let fuck_word = match verbosity {
1 => "frick",
2 => "flock",
3 => "fock",
4 => "fck",
_ => "fuck",
};
for _ in 0..amount {
msg.channel_id
.say(&ctx, format!("{} <@{}>", fuck_word, msg.author.id))
.await?;
}
Ok(())
}

@ -1,4 +1,4 @@
use crate::messages::gifs::create_media_menu; use crate::messages::gifs::create_gifs_menu;
use crate::utils::context_data::get_database_from_context; use crate::utils::context_data::get_database_from_context;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
@ -9,10 +9,10 @@ use serenity::model::channel::Message;
#[description("Displays a list of all gifs used by the bot")] #[description("Displays a list of all gifs used by the bot")]
#[bucket("general")] #[bucket("general")]
#[only_in(guilds)] #[only_in(guilds)]
async fn media(ctx: &Context, msg: &Message) -> CommandResult { async fn gifs(ctx: &Context, msg: &Message) -> CommandResult {
let database = get_database_from_context(ctx).await; let database = get_database_from_context(ctx).await;
let gifs = database.get_all_media().await?; let gifs = database.get_all_gifs().await?;
create_media_menu(ctx, msg.channel_id, gifs, msg.author.id).await?; create_gifs_menu(ctx, msg.channel_id, gifs).await?;
Ok(()) Ok(())
} }

@ -19,7 +19,6 @@ pub async fn help(
groups: &[&'static CommandGroup], groups: &[&'static CommandGroup],
owners: HashSet<UserId>, owners: HashSet<UserId>,
) -> CommandResult { ) -> CommandResult {
tracing::debug!("Help");
let _ = help_commands::with_embeds(ctx, msg, args, help_options, groups, owners).await; let _ = help_commands::with_embeds(ctx, msg, args, help_options, groups, owners).await;
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;
Ok(()) Ok(())

@ -1,16 +0,0 @@
use crate::messages::inspirobot::create_inspirobot_menu;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message;
#[command]
#[description("Get an inspiring quote")]
#[usage("")]
#[aliases("inspireme", "inspire-me", "inspiro")]
#[bucket("general")]
async fn inspirobot(ctx: &Context, msg: &Message) -> CommandResult {
create_inspirobot_menu(ctx, msg.channel_id, msg.author.id).await?;
Ok(())
}

@ -1,41 +1,30 @@
use serenity::framework::standard::macros::group; use serenity::framework::standard::macros::group;
use about::ABOUT_COMMAND; use about::ABOUT_COMMAND;
use add_media::ADD_MEDIA_COMMAND; use add_gif::ADD_GIF_COMMAND;
use clear::CLEAR_COMMAND; use gifs::GIFS_COMMAND;
use fuck::FUCK_COMMAND;
use inspirobot::INSPIROBOT_COMMAND;
use media::MEDIA_COMMAND;
use pain::PAIN_COMMAND; use pain::PAIN_COMMAND;
use party::PARTY_COMMAND;
use ping::PING_COMMAND; use ping::PING_COMMAND;
use qalc::QALC_COMMAND; use qalc::QALC_COMMAND;
use shutdown::SHUTDOWN_COMMAND; use shutdown::SHUTDOWN_COMMAND;
use stats::STATS_COMMAND; use stats::STATS_COMMAND;
use time::TIME_COMMAND; use time::TIME_COMMAND;
use timezones::TIMEZONES_COMMAND; use timezones::TIMEZONES_COMMAND;
use xkcd::XKCD_COMMAND;
mod about; mod about;
mod add_media; mod add_gif;
mod clear; mod gifs;
mod fuck;
pub(crate) mod help; pub(crate) mod help;
mod inspirobot;
mod media;
mod pain; mod pain;
mod party;
mod ping; mod ping;
mod qalc; mod qalc;
mod shutdown; mod shutdown;
mod stats; mod stats;
mod time; mod time;
mod timezones; mod timezones;
mod xkcd;
#[group] #[group]
#[commands( #[commands(
ping, stats, shutdown, time, timezones, qalc, about, add_media, media, pain, clear, xkcd, fuck, ping, stats, shutdown, time, timezones, qalc, about, add_gif, gifs, pain
party, inspirobot
)] )]
pub struct Misc; pub struct Misc;

@ -17,26 +17,26 @@ static NOT_FOUND_PAIN: &str = "404";
#[max_args(1)] #[max_args(1)]
#[bucket("general")] #[bucket("general")]
async fn pain(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn pain(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
tracing::debug!("Got pain command"); log::debug!("Got pain command");
let pain_type = args.message().to_lowercase(); let pain_type = args.message().to_lowercase();
let database = get_database_from_context(ctx).await; let database = get_database_from_context(ctx).await;
let mut media = database let mut gifs = database
.get_media_by_category(format!("{}{}", CATEGORY_PREFIX, pain_type)) .get_gifs_by_category(format!("{}{}", CATEGORY_PREFIX, pain_type).as_str())
.await?; .await?;
if media.is_empty() { if gifs.is_empty() {
tracing::debug!("No media found for pain {}. Using 404", pain_type); log::debug!("No gif found for pain {}. Using 404", pain_type);
media = database gifs = database
.get_media_by_category(format!("{}{}", CATEGORY_PREFIX, NOT_FOUND_PAIN)) .get_gifs_by_category(format!("{}{}", CATEGORY_PREFIX, NOT_FOUND_PAIN).as_str())
.await?; .await?;
} }
let entry = media let gif = gifs
.into_iter() .into_iter()
.choose(&mut rand::thread_rng()) .choose(&mut rand::thread_rng())
.ok_or(BotError::from("No gifs found."))?; .ok_or(BotError::from("No gifs found."))?;
tracing::trace!("Gif for pain is {:?}", entry); log::trace!("Gif for pain is {:?}", gif);
msg.reply(ctx, entry.url).await?; msg.reply(ctx, gif.url).await?;
Ok(()) Ok(())
} }

@ -1,43 +0,0 @@
use crate::utils::context_data::get_database_from_context;
use crate::utils::error::BotError;
use bot_database::models::Media;
use rand::prelude::SliceRandom;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
#[command]
#[description("Party command")]
#[max_args(1)]
#[usage("(<amount>)")]
#[bucket("general")]
async fn party(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let mut amount = args.single::<u32>().unwrap_or(1);
if amount > 5 {
amount = 5;
}
if amount == 0 {
return Ok(());
}
let database = get_database_from_context(ctx).await;
let mut media = database.get_media_by_category("party").await?;
media.shuffle(&mut rand::thread_rng());
let mut chosen_gifs = Vec::new();
for _ in 0..amount {
chosen_gifs.push(media.pop());
}
let chosen_gifs: Vec<Media> = chosen_gifs.into_iter().filter_map(|g| g).collect();
if chosen_gifs.is_empty() {
return Err(BotError::from("No media found.").into());
}
for gif in chosen_gifs {
msg.channel_id
.send_message(&ctx.http, |m| m.content(gif.url))
.await?;
}
Ok(())
}

@ -15,7 +15,7 @@ use crate::commands::common::handle_autodelete;
#[owners_only] #[owners_only]
async fn shutdown(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn shutdown(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let code = args.single::<i32>().unwrap_or(0); let code = args.single::<i32>().unwrap_or(0);
tracing::info!("Shutting down with code {}...", code); log::info!("Shutting down with code {}...", code);
msg.channel_id msg.channel_id
.say( .say(
ctx, ctx,

@ -1,28 +1,30 @@
use std::process; use std::process;
use std::time::Duration; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use chrono::Duration as ChronoDuration; use chrono::Duration as ChronoDuration;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::CommandResult; use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::prelude::*; use serenity::prelude::*;
use sysinfo::{Pid, PidExt, ProcessExt, SystemExt}; use sysinfo::{ProcessExt, SystemExt};
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::utils::context_data::{get_database_from_context, MusicPlayers}; use crate::providers::music::queue::MusicQueue;
use crate::utils::context_data::{get_database_from_context, Store};
use std::sync::Arc;
#[command] #[command]
#[description("Shows some statistics about the bot")] #[description("Shows some statistics about the bot")]
#[usage("")] #[usage("")]
#[bucket("general")] #[bucket("general")]
async fn stats(ctx: &Context, msg: &Message) -> CommandResult { async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
tracing::debug!("Reading system stats"); log::debug!("Reading system stats");
let database = get_database_from_context(ctx).await; let database = get_database_from_context(ctx).await;
let mut system = sysinfo::System::new_all(); let mut system = sysinfo::System::new_all();
system.refresh_all(); system.refresh_all();
let kernel_version = system.kernel_version().unwrap_or("n/a".to_string()); let kernel_version = system.get_kernel_version().unwrap_or("n/a".to_string());
let own_process = system.process(Pid::from_u32(process::id())).unwrap(); let own_process = system.get_process(process::id() as i32).unwrap();
let memory_usage = own_process.memory(); let memory_usage = own_process.memory();
let cpu_usage = own_process.cpu_usage(); let cpu_usage = own_process.cpu_usage();
let thread_count = own_process.tasks.len(); let thread_count = own_process.tasks.len();
@ -30,16 +32,16 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
let guild_count: usize = current_user.guilds(ctx).await?.len(); let guild_count: usize = current_user.guilds(ctx).await?.len();
let bot_info = ctx.http.get_current_application_info().await?; let bot_info = ctx.http.get_current_application_info().await?;
let uptime = own_process.run_time(); let current_time_seconds = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let uptime = ChronoDuration::from_std(Duration::from_secs(uptime)).unwrap(); let uptime = current_time_seconds - Duration::from_secs(own_process.start_time());
let uptime = ChronoDuration::from_std(uptime).unwrap();
let total_commands_executed = database.get_total_commands_statistic().await?; let total_commands_executed = database.get_total_commands_statistic().await?;
let shard_count = ctx.cache.shard_count(); let shard_count = ctx.cache.shard_count().await;
let discord_info = format!( let discord_info = format!(
r#" r#"
Version: {} Version: {}
Compiled with: rustc {} Compiled with: rustc {}
Build at: {}
Owner: <@{}> Owner: <@{}>
Guilds: {} Guilds: {}
Shards: {} Shards: {}
@ -48,7 +50,6 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
"#, "#,
crate::VERSION, crate::VERSION,
rustc_version_runtime::version(), rustc_version_runtime::version(),
build_time::build_time_utc!("%Y-%m-%dT%H:%M:%S"),
bot_info.owner.id, bot_info.owner.id,
guild_count, guild_count,
shard_count, shard_count,
@ -56,7 +57,7 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
total_commands_executed total_commands_executed
); );
tracing::trace!("Discord info {}", discord_info); log::trace!("Discord info {}", discord_info);
let system_info = format!( let system_info = format!(
r#" r#"
@ -74,7 +75,7 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
uptime.num_hours() % 24, uptime.num_hours() % 24,
uptime.num_minutes() % 60 uptime.num_minutes() % 60
); );
tracing::trace!("System info {}", system_info); log::trace!("System info {}", system_info);
msg.channel_id msg.channel_id
.send_message(ctx, |m| { .send_message(ctx, |m| {
@ -93,7 +94,23 @@ async fn stats(ctx: &Context, msg: &Message) -> CommandResult {
/// Returns the total number of queues that are not /// Returns the total number of queues that are not
/// flagged to leave /// flagged to leave
async fn get_queue_count(ctx: &Context) -> usize { async fn get_queue_count(ctx: &Context) -> usize {
let data = ctx.data.read().await; let queues: Vec<Arc<Mutex<MusicQueue>>> = {
let players = data.get::<MusicPlayers>().unwrap(); let data = ctx.data.read().await;
players.len() let store = data.get::<Store>().unwrap();
store
.music_queues
.iter()
.map(|(_, q)| Arc::clone(q))
.collect()
};
let mut count = 0;
for queue in queues {
let queue = queue.lock().await;
if !queue.leave_flag {
count += 1;
}
}
count
} }

@ -17,13 +17,13 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let second_timezone = args.single::<String>().ok(); let second_timezone = args.single::<String>().ok();
let from_timezone: Tz = if let Some(first) = &first_timezone { let from_timezone: Tz = if let Some(first) = &first_timezone {
crate::forward_error!(ctx, msg.channel_id, first.parse::<Tz>()) forward_error!(ctx, msg.channel_id, first.parse::<Tz>())
} else { } else {
Tz::UTC Tz::UTC
}; };
let to_timezone = if let Some(second) = &second_timezone { let to_timezone = if let Some(second) = &second_timezone {
crate::forward_error!(ctx, msg.channel_id, second.parse::<Tz>()) forward_error!(ctx, msg.channel_id, second.parse::<Tz>())
} else { } else {
Tz::UTC Tz::UTC
}; };
@ -33,7 +33,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
} else { } else {
let now = Utc::now(); let now = Utc::now();
if second_timezone.is_some() { if second_timezone.is_some() {
crate::forward_error!( forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
from_timezone.datetime_from_str( from_timezone.datetime_from_str(
@ -43,7 +43,7 @@ async fn time(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
) )
} else { } else {
let timezone: Tz = "UTC".parse().unwrap(); let timezone: Tz = "UTC".parse().unwrap();
crate::forward_error!( forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
timezone.datetime_from_str( timezone.datetime_from_str(

@ -1,35 +0,0 @@
use crate::messages::xkcd::create_xkcd_menu;
use serenity::client::Context;
use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message;
use xkcd_search::get_comic;
#[command]
#[description("Retrieves xkcd comics")]
#[usage("[(<id>|<query..>)]")]
#[bucket("general")]
async fn xkcd(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let comics = if let Ok(id) = args.single::<u32>() {
if let Ok(comic) = xkcd_search::get_comic(id).await {
vec![comic]
} else {
vec![]
}
} else if !args.is_empty() {
let query = args.message();
let results = xkcd_search::search(query).await?;
let comics =
futures::future::join_all(results.into_iter().map(|(_, id)| get_comic(id))).await;
comics
.into_iter()
.filter_map(|result| result.ok())
.collect()
} else {
vec![xkcd_search::get_latest_comic().await?]
};
create_xkcd_menu(ctx, msg.channel_id, comics, msg.author.id).await?;
Ok(())
}

@ -1,13 +1,12 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandError, CommandResult}; use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_queue_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT;
use serenity_additions::core::SHORT_TIMEOUT; use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -17,19 +16,17 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[bucket("general")] #[bucket("general")]
#[checks(DJ)] #[checks(DJ)]
async fn clear_queue(ctx: &Context, msg: &Message) -> CommandResult { async fn clear_queue(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Clearing queue for guild {}", guild.id); log::debug!("Clearing queue for guild {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { let queue = forward_error!(
player ctx,
} else { msg.channel_id,
return create_no_voicechannel_message(&ctx.http, msg.channel_id) get_queue_for_guild(ctx, &guild.id).await
.await );
.map_err(CommandError::from);
};
{ {
let mut player = player.lock().await; let mut queue_lock = queue.lock().await;
player.queue().clear(); queue_lock.clear();
} }
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

@ -1,11 +1,12 @@
use std::mem;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandError, CommandResult}; use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::get_music_player_for_guild; use crate::commands::music::get_queue_for_guild;
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use crate::messages::music::now_playing::create_now_playing_msg; use crate::messages::music::now_playing::create_now_playing_msg;
#[command] #[command]
@ -15,25 +16,32 @@ use crate::messages::music::now_playing::create_now_playing_msg;
#[aliases("nowplaying", "np")] #[aliases("nowplaying", "np")]
#[bucket("general")] #[bucket("general")]
async fn current(ctx: &Context, msg: &Message) -> CommandResult { async fn current(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Displaying current song for queue in {}", guild.id); log::debug!("Displaying current song for queue in {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { let queue = forward_error!(
player ctx,
} else { msg.channel_id,
return create_no_voicechannel_message(&ctx.http, msg.channel_id) get_queue_for_guild(ctx, &guild.id).await
.await );
.map_err(CommandError::from);
};
let current = { let current = {
let mut player = player.lock().await; let queue_lock = queue.lock().await;
player.queue().current().clone() queue_lock.current().clone()
}; };
if let Some(_) = current { if let Some((current, _)) = current {
let np_msg = create_now_playing_msg(ctx, player.clone(), msg.channel_id).await?; let metadata = current.metadata().clone();
let mut player = player.lock().await; log::trace!("Metadata is {:?}", metadata);
player.set_now_playing(np_msg).await; let np_msg = create_now_playing_msg(ctx, queue.clone(), msg.channel_id).await?;
let mut queue_lock = queue.lock().await;
if let Some(old_np) = mem::replace(&mut queue_lock.now_playing_msg, Some(np_msg)) {
let old_np = old_np.read().await;
if let Ok(message) = old_np.get_message(&ctx.http).await {
let _ = message.delete(ctx).await;
}
}
} }
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;

@ -4,11 +4,10 @@ use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_channel_for_author, get_music_player_for_guild, is_dj}; use crate::commands::music::{get_channel_for_author, is_dj, join_channel};
use crate::providers::music::player::MusicPlayer; use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::model::id::ChannelId; use serenity::model::id::ChannelId;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -16,33 +15,26 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[usage("")] #[usage("")]
#[bucket("general")] #[bucket("general")]
async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn join(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
let channel_id = if let Ok(arg) = args.single::<u64>() { let channel_id = if let Ok(arg) = args.single::<u64>() {
if is_dj(ctx, guild.id, &msg.author).await? { if is_dj(ctx, guild.id, &msg.author).await? {
ChannelId(arg) ChannelId(arg)
} else { } else {
crate::forward_error!( forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
get_channel_for_author(&msg.author.id, &guild) get_channel_for_author(&msg.author.id, &guild)
) )
} }
} else { } else {
crate::forward_error!( forward_error!(
ctx, ctx,
msg.channel_id, msg.channel_id,
get_channel_for_author(&msg.author.id, &guild) get_channel_for_author(&msg.author.id, &guild)
) )
}; };
if get_music_player_for_guild(ctx, guild.id).await.is_some() { log::debug!("Joining channel {} for guild {}", channel_id, guild.id);
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { join_channel(ctx, channel_id, guild.id).await;
m.content("‼️ I'm already in a Voice Channel")
})
.await?;
return Ok(());
}
tracing::debug!("Joining channel {} for guild {}", channel_id, guild.id);
MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("🎤 Joined the Voice Channel") m.content("🎤 Joined the Voice Channel")
}) })

@ -4,10 +4,10 @@ use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::DJ_CHECK; use crate::commands::music::{get_voice_manager, DJ_CHECK};
use crate::utils::context_data::MusicPlayers; use crate::utils::context_data::Store;
use serenity_additions::core::SHORT_TIMEOUT; use bot_serenityutils::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage; use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -17,33 +17,43 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[bucket("general")] #[bucket("general")]
#[checks(DJ)] #[checks(DJ)]
async fn leave(ctx: &Context, msg: &Message) -> CommandResult { async fn leave(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Leave request received for guild {}", guild.id); log::debug!("Leave request received for guild {}", guild.id);
let manager = songbird::get(ctx).await.unwrap(); let manager = get_voice_manager(ctx).await;
if let Some(handler) = manager.get(guild.id) { let queue = {
let mut data = ctx.data.write().await;
let store = data.get_mut::<Store>().unwrap();
store
.music_queues
.remove(&guild.id)
.expect("No queue for guild.")
};
let queue_lock = queue.lock().await;
let handler = manager.get(guild.id);
if let Some(handler) = handler {
let mut handler_lock = handler.lock().await; let mut handler_lock = handler.lock().await;
handler_lock.leave().await?; handler_lock.remove_all_global_events();
} }
let mut data = ctx.data.write().await; if manager.get(guild.id).is_some() {
let players = data.get_mut::<MusicPlayers>().unwrap(); if let Some((current, _)) = queue_lock.current() {
current.stop()?;
match players.remove(&guild.id.0) {
None => {
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("‼️ I'm not in a Voice Channel")
})
.await?;
}
Some(player) => {
let mut player = player.lock().await;
player.stop().await?;
player.delete_now_playing().await?;
} }
manager.remove(guild.id).await?;
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("👋 Left the Voice Channel")
})
.await?;
log::debug!("Left the voice channel");
} else {
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("‼️ I'm not in a Voice Channel")
})
.await?;
log::debug!("Not in a voice channel");
} }
manager.remove(guild.id).await?;
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;
Ok(()) Ok(())

@ -1,11 +1,11 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandError, CommandResult}; use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::get_music_player_for_guild; use crate::commands::music::get_queue_for_guild;
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::providers::music::lyrics::get_lyrics;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -13,43 +13,43 @@ use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
#[usage("")] #[usage("")]
#[bucket("general")] #[bucket("general")]
async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult { async fn lyrics(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Fetching lyrics for song playing in {}", guild.id); log::debug!("Fetching lyrics for song playing in {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { let queue = forward_error!(
player ctx,
} else { msg.channel_id,
return create_no_voicechannel_message(&ctx.http, msg.channel_id) get_queue_for_guild(ctx, &guild.id).await
.await );
.map_err(CommandError::from); let queue_lock = queue.lock().await;
};
let (lyrics, current) = { if let Some((current, _)) = queue_lock.current() {
let mut player = player.lock().await; log::debug!("Playing music. Fetching lyrics for currently playing song...");
let current = player.queue().current().clone(); let metadata = current.metadata();
(player.lyrics().await?, current) let title = metadata.title.clone().unwrap();
}; let author = metadata.artist.clone().unwrap();
if let Some(lyrics) = lyrics { if let Some(lyrics) = get_lyrics(&*author, &*title).await? {
let current = current.unwrap(); log::trace!("Lyrics for '{}' are {}", title, lyrics);
msg.channel_id
.send_message(ctx, |m| { msg.channel_id
m.embed(|e| { .send_message(ctx, |m| {
e.title(format!( m.embed(|e| {
"Lyrics for {} by {}", e.title(format!("Lyrics for {} by {}", title, author))
current.title(), .description(lyrics)
current.author() .footer(|f| f.text("Powered by lyricsovh"))
)) })
.description(lyrics)
.footer(|f| f.text("Powered by lyricsovh"))
}) })
}) .await?;
.await?; } else {
log::debug!("No lyrics found");
msg.channel_id.say(ctx, "No lyrics found").await?;
}
} else { } else {
tracing::debug!("No lyrics found"); msg.channel_id
msg.channel_id.say(ctx, "No lyrics found").await?; .say(ctx, "I'm not playing music right now")
.await?;
} }
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;
Ok(()) Ok(())

@ -1,20 +1,22 @@
use std::mem;
use std::sync::atomic::{AtomicIsize, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use aspotify::Track; use aspotify::Track;
use bot_database::Database;
use futures::future::BoxFuture;
use futures::FutureExt;
use regex::Regex; use regex::Regex;
use serenity::async_trait;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::{check, group}; use serenity::framework::standard::macros::{check, group};
use serenity::framework::standard::{Args, CommandOptions, Reason}; use serenity::http::Http;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::model::guild::Guild; use serenity::model::guild::Guild;
use serenity::model::id::{ChannelId, GuildId, UserId}; use serenity::model::id::{ChannelId, GuildId, UserId};
use serenity::model::user::User; use serenity::model::user::User;
use songbird::Songbird; use songbird::{
Call, Event, EventContext, EventHandler as VoiceEventHandler, Songbird, TrackEvent,
};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use youtube_metadata::get_video_information;
use clear_queue::CLEAR_QUEUE_COMMAND; use clear_queue::CLEAR_QUEUE_COMMAND;
use current::CURRENT_COMMAND; use current::CURRENT_COMMAND;
@ -32,12 +34,14 @@ use save_playlist::SAVE_PLAYLIST_COMMAND;
use shuffle::SHUFFLE_COMMAND; use shuffle::SHUFFLE_COMMAND;
use skip::SKIP_COMMAND; use skip::SKIP_COMMAND;
use crate::providers::music::player::MusicPlayer; use crate::messages::music::now_playing::update_now_playing_msg;
use crate::providers::music::queue::Song; use crate::providers::music::queue::{MusicQueue, Song};
use crate::providers::music::{add_youtube_song_to_database, youtube_dl}; use crate::providers::music::{add_youtube_song_to_database, youtube_dl};
use crate::providers::settings::{get_setting, Setting}; use crate::providers::settings::{get_setting, Setting};
use crate::utils::context_data::{DatabaseContainer, MusicPlayers, Store}; use crate::utils::context_data::{DatabaseContainer, Store};
use crate::utils::error::{BotError, BotResult}; use crate::utils::error::{BotError, BotResult};
use bot_database::Database;
use serenity::framework::standard::{Args, CommandOptions, Reason};
mod clear_queue; mod clear_queue;
mod current; mod current;
@ -75,12 +79,116 @@ mod skip;
)] )]
pub struct Music; pub struct Music;
/// Returns the voice manager from the context struct SongEndNotifier {
pub async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> { channel_id: ChannelId,
songbird::get(ctx) http: Arc<Http>,
queue: Arc<Mutex<MusicQueue>>,
handler: Arc<Mutex<Call>>,
}
#[async_trait]
impl VoiceEventHandler for SongEndNotifier {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
log::debug!("Song ended in {}. Playing next one", self.channel_id);
while !play_next_in_queue(&self.http, &self.channel_id, &self.queue, &self.handler).await {
tokio::time::sleep(Duration::from_millis(100)).await;
}
None
}
}
struct ChannelDurationNotifier {
channel_id: ChannelId,
guild_id: GuildId,
count: Arc<AtomicUsize>,
queue: Arc<Mutex<MusicQueue>>,
leave_in: Arc<AtomicIsize>,
handler: Arc<Mutex<Call>>,
manager: Arc<Songbird>,
}
#[async_trait]
impl VoiceEventHandler for ChannelDurationNotifier {
async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
let count_before = self.count.fetch_add(1, Ordering::Relaxed);
log::debug!(
"Playing in channel {} for {} minutes",
self.channel_id,
count_before
);
let queue_lock = self.queue.lock().await;
if queue_lock.leave_flag {
log::debug!("Waiting to leave");
if self.leave_in.fetch_sub(1, Ordering::Relaxed) <= 0 {
log::debug!("Leaving voice channel");
{
let mut handler_lock = self.handler.lock().await;
handler_lock.remove_all_global_events();
}
if let Some((current, _)) = queue_lock.current() {
let _ = current.stop();
}
let _ = self.manager.remove(self.guild_id).await;
log::debug!("Left the voice channel");
}
} else {
log::debug!("Resetting leave value");
self.leave_in.store(5, Ordering::Relaxed)
}
None
}
}
/// Joins a voice channel
async fn join_channel(ctx: &Context, channel_id: ChannelId, guild_id: GuildId) -> Arc<Mutex<Call>> {
log::debug!(
"Attempting to join channel {} in guild {}",
channel_id,
guild_id
);
let manager = songbird::get(ctx)
.await .await
.expect("Songbird Voice client placed in at initialisation.") .expect("Songbird Voice client placed in at initialisation.")
.clone() .clone();
let (handler, _) = manager.join(guild_id, channel_id).await;
let mut data = ctx.data.write().await;
let store = data.get_mut::<Store>().unwrap();
log::debug!("Creating new queue");
let queue = Arc::new(Mutex::new(MusicQueue::new()));
store.music_queues.insert(guild_id, queue.clone());
{
let mut handler_lock = handler.lock().await;
log::debug!("Registering track end handler");
handler_lock.add_global_event(
Event::Track(TrackEvent::End),
SongEndNotifier {
channel_id: channel_id.clone(),
http: ctx.http.clone(),
queue: Arc::clone(&queue),
handler: handler.clone(),
},
);
handler_lock.add_global_event(
Event::Periodic(Duration::from_secs(60), None),
ChannelDurationNotifier {
channel_id,
guild_id,
count: Default::default(),
queue: Arc::clone(&queue),
handler: handler.clone(),
leave_in: Arc::new(AtomicIsize::new(5)),
manager: manager.clone(),
},
);
}
handler
} }
/// Returns the voice channel the author is in /// Returns the voice channel the author is in
@ -92,15 +200,88 @@ fn get_channel_for_author(author_id: &UserId, guild: &Guild) -> BotResult<Channe
.ok_or(BotError::from("You're not in a Voice Channel")) .ok_or(BotError::from("You're not in a Voice Channel"))
} }
/// Returns the music player for a given guild /// Returns the voice manager from the context
pub async fn get_music_player_for_guild( pub async fn get_voice_manager(ctx: &Context) -> Arc<Songbird> {
songbird::get(ctx)
.await
.expect("Songbird Voice client placed in at initialisation.")
.clone()
}
/// Returns a reference to a guilds music queue
pub(crate) async fn get_queue_for_guild(
ctx: &Context, ctx: &Context,
guild_id: GuildId, guild_id: &GuildId,
) -> Option<Arc<Mutex<MusicPlayer>>> { ) -> BotResult<Arc<Mutex<MusicQueue>>> {
let data = ctx.data.read().await; let data = ctx.data.read().await;
let players = data.get::<MusicPlayers>().unwrap(); let store = data.get::<Store>().unwrap();
players.get(&guild_id.0).cloned() let queue = store
.music_queues
.get(guild_id)
.ok_or(BotError::from("I'm not in a Voice Channel"))?
.clone();
Ok(queue)
}
/// Plays the next song in the queue
async fn play_next_in_queue(
http: &Arc<Http>,
channel_id: &ChannelId,
queue: &Arc<Mutex<MusicQueue>>,
handler: &Arc<Mutex<Call>>,
) -> bool {
let mut queue_lock = queue.lock().await;
if let Some(mut next) = queue_lock.next() {
let url = match next.url().await {
Some(url) => url,
None => {
let _ = channel_id
.say(&http, format!("'{}' not found", next.title()))
.await;
return false;
}
};
log::debug!("Getting source for song '{}'", url);
let source = match songbird::ytdl(&url).await {
Ok(s) => s,
Err(e) => {
let _ = channel_id
.say(
&http,
format!("Failed to enqueue {}: {:?}", next.title(), e),
)
.await;
return false;
}
};
let mut handler_lock = handler.lock().await;
let track = handler_lock.play_only_source(source);
log::trace!("Track is {:?}", track);
if queue_lock.paused() {
let _ = track.pause();
}
if let Some(np) = &queue_lock.now_playing_msg {
if let Err(e) =
update_now_playing_msg(http, np, track.metadata(), queue_lock.paused()).await
{
log::error!("Failed to update now playing message: {:?}", e);
}
}
queue_lock.set_current(track, next);
} else {
if let Some(np) = mem::take(&mut queue_lock.now_playing_msg) {
let np = np.read().await;
if let Ok(message) = np.get_message(http).await {
let _ = message.delete(http).await;
}
}
queue_lock.clear_current();
}
true
} }
/// Returns the list of songs for a given url /// Returns the list of songs for a given url
@ -120,23 +301,23 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
let store = data.get::<Store>().unwrap(); let store = data.get::<Store>().unwrap();
let database = data.get::<DatabaseContainer>().unwrap(); let database = data.get::<DatabaseContainer>().unwrap();
tracing::debug!("Querying play input {}", query); log::debug!("Querying play input {}", query);
if let Some(captures) = PLAYLIST_NAME_REGEX.captures(&query) { if let Some(captures) = PLAYLIST_NAME_REGEX.captures(&query) {
tracing::debug!("Query is a saved playlist"); log::debug!("Query is a saved playlist");
let pl_name: &str = captures.get(1).unwrap().as_str(); let pl_name: &str = captures.get(1).unwrap().as_str();
tracing::trace!("Playlist name is {}", pl_name); log::trace!("Playlist name is {}", pl_name);
let playlist_opt = database let playlist_opt = database
.get_guild_playlist(guild_id.0, pl_name.to_string()) .get_guild_playlist(guild_id.0, pl_name.to_string())
.await?; .await?;
tracing::trace!("Playlist is {:?}", playlist_opt); log::trace!("Playlist is {:?}", playlist_opt);
if let Some(playlist) = playlist_opt { if let Some(playlist) = playlist_opt {
tracing::debug!("Assigning url for saved playlist to query"); log::debug!("Assigning url for saved playlist to query");
query = playlist.url; query = playlist.url;
} }
} }
if YOUTUBE_URL_REGEX.is_match(&query) { if YOUTUBE_URL_REGEX.is_match(&query) {
tracing::debug!("Query is youtube video or playlist"); log::debug!("Query is youtube video or playlist");
// try fetching the url as a playlist // try fetching the url as a playlist
songs = youtube_dl::get_videos_for_playlist(&query) songs = youtube_dl::get_videos_for_playlist(&query)
.await? .await?
@ -146,38 +327,32 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
// if no songs were found fetch the song as a video // if no songs were found fetch the song as a video
if songs.len() == 0 { if songs.len() == 0 {
tracing::debug!("Query is youtube video"); log::debug!("Query is youtube video");
let mut song: Song = get_video_information(&query).await?.into(); let mut song: Song = youtube_dl::get_video_information(&query).await?.into();
added_one_msg(&ctx, msg, &mut song).await?; added_one_msg(&ctx, msg, &mut song).await?;
add_youtube_song_to_database(&store, &database, &mut song).await?; add_youtube_song_to_database(&store, &database, &mut song).await?;
songs.push(song); songs.push(song);
} else { } else {
tracing::debug!("Query is playlist with {} songs", songs.len()); log::debug!("Query is playlist with {} songs", songs.len());
added_multiple_msg(&ctx, msg, &mut songs).await?; added_multiple_msg(&ctx, msg, &mut songs).await?;
} }
} else if SPOTIFY_PLAYLIST_REGEX.is_match(&query) { } else if SPOTIFY_PLAYLIST_REGEX.is_match(&query) {
// search for all songs in the playlist and search for them on youtube // search for all songs in the playlist and search for them on youtube
tracing::debug!("Query is spotify playlist"); log::debug!("Query is spotify playlist");
let tracks = store.spotify_api.get_songs_in_playlist(&query).await?; let tracks = store.spotify_api.get_songs_in_playlist(&query).await?;
let futures: Vec<BoxFuture<Song>> = tracks for track in tracks {
.into_iter() songs.push(
.map(|track| { get_youtube_song_for_track(&database, track.clone())
async { .await?
get_youtube_song_for_track(&database, track.clone()) .unwrap_or(track.into()),
.await )
.unwrap_or(None) }
.unwrap_or(track.into())
}
.boxed()
})
.collect();
songs = futures::future::join_all(futures).await;
added_multiple_msg(&ctx, msg, &mut songs).await?; added_multiple_msg(&ctx, msg, &mut songs).await?;
} else if SPOTIFY_ALBUM_REGEX.is_match(&query) { } else if SPOTIFY_ALBUM_REGEX.is_match(&query) {
// fetch all songs in the album and search for them on youtube // fetch all songs in the album and search for them on youtube
tracing::debug!("Query is spotify album"); log::debug!("Query is spotify album");
let tracks = store.spotify_api.get_songs_in_album(&query).await?; let tracks = store.spotify_api.get_songs_in_album(&query).await?;
for track in tracks { for track in tracks {
@ -191,7 +366,7 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
added_multiple_msg(&ctx, msg, &mut songs).await?; added_multiple_msg(&ctx, msg, &mut songs).await?;
} else if SPOTIFY_SONG_REGEX.is_match(&query) { } else if SPOTIFY_SONG_REGEX.is_match(&query) {
// fetch the song name and search it on youtube // fetch the song name and search it on youtube
tracing::debug!("Query is a spotify song"); log::debug!("Query is a spotify song");
let track = store.spotify_api.get_track_for_url(&query).await?; let track = store.spotify_api.get_track_for_url(&query).await?;
let mut song = get_youtube_song_for_track(&database, track.clone()) let mut song = get_youtube_song_for_track(&database, track.clone())
.await? .await?
@ -199,12 +374,12 @@ async fn get_songs_for_query(ctx: &Context, msg: &Message, query: &str) -> BotRe
added_one_msg(ctx, msg, &mut song).await?; added_one_msg(ctx, msg, &mut song).await?;
songs.push(song); songs.push(song);
} else { } else {
tracing::debug!("Query is a youtube search"); log::debug!("Query is a youtube search");
let mut song: Song = youtube_dl::search_video_information(query.clone()) let mut song: Song = youtube_dl::search_video_information(query.clone())
.await? .await?
.ok_or(BotError::Msg(format!("Noting found for {}", query)))? .ok_or(BotError::Msg(format!("Noting found for {}", query)))?
.into(); .into();
tracing::trace!("Search result is {:?}", song); log::trace!("Search result is {:?}", song);
added_one_msg(&ctx, msg, &mut song).await?; added_one_msg(&ctx, msg, &mut song).await?;
songs.push(song); songs.push(song);
@ -251,6 +426,7 @@ pub async fn check_dj(
) -> Result<(), Reason> { ) -> Result<(), Reason> {
let guild = msg let guild = msg
.guild(&ctx.cache) .guild(&ctx.cache)
.await
.ok_or(Reason::Log("Not in a guild".to_string()))?; .ok_or(Reason::Log("Not in a guild".to_string()))?;
if is_dj(ctx, guild.id, &msg.author) if is_dj(ctx, guild.id, &msg.author)
@ -284,24 +460,22 @@ pub async fn is_dj(ctx: &Context, guild: GuildId, user: &User) -> BotResult<bool
/// Searches for a matching youtube song for the given track in the local database /// Searches for a matching youtube song for the given track in the local database
async fn get_youtube_song_for_track(database: &Database, track: Track) -> BotResult<Option<Song>> { async fn get_youtube_song_for_track(database: &Database, track: Track) -> BotResult<Option<Song>> {
tracing::debug!("Trying to find track in database."); log::debug!("Trying to find track in database.");
if let Some(id) = track.id { if let Some(id) = track.id {
let entry = database.get_song(&id).await?; let entry = database.get_song(&id).await?;
if let Some(song) = entry { if let Some(song) = &entry {
// check if the video is still available // check if the video is still available
tracing::trace!("Found entry is {:?}", song); if youtube_dl::get_video_information(&song.url).await.is_err() {
if let Ok(info) = get_video_information(&song.url).await { log::debug!("Video '{}' is not available. Deleting entry", song.url);
return Ok(Some(info.into()));
} else {
tracing::debug!("Video '{}' is not available. Deleting entry", song.url);
database.delete_song(song.id).await?; database.delete_song(song.id).await?;
return Ok(None); return Ok(None);
} }
} }
Ok(None) log::trace!("Found entry is {:?}", entry);
Ok(entry.map(Song::from))
} else { } else {
tracing::debug!("Track has no ID"); log::debug!("Track has no ID");
Ok(None) Ok(None)
} }
} }

@ -1,12 +1,11 @@
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_queue_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Moves a song in the queue from one position to a new one")] #[description("Moves a song in the queue from one position to a new one")]
@ -18,22 +17,20 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[aliases("mvs", "movesong", "move-song")] #[aliases("mvs", "movesong", "move-song")]
#[checks(DJ)] #[checks(DJ)]
async fn move_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn move_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Moving song for guild {}", guild.id); log::debug!("Moving song for guild {}", guild.id);
let pos1 = args.single::<usize>()?; let pos1 = args.single::<usize>()?;
let pos2 = args.single::<usize>()?; let pos2 = args.single::<usize>()?;
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {
player
} else {
return create_no_voicechannel_message(&ctx.http, msg.channel_id)
.await
.map_err(CommandError::from);
};
{ {
let mut player = player.lock().await; let queue = forward_error!(
player.queue().move_position(pos1, pos2); ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
let mut queue_lock = queue.lock().await;
queue_lock.move_position(pos1, pos2);
} }
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content(format!( m.content(format!(

@ -1,13 +1,13 @@
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{CommandError, CommandResult}; use serenity::framework::standard::CommandResult;
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity::prelude::*; use serenity::prelude::*;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_queue_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use crate::messages::music::now_playing::update_now_playing_msg;
use serenity_additions::core::SHORT_TIMEOUT; use bot_serenityutils::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage; use bot_serenityutils::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -16,36 +16,40 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[bucket("general")] #[bucket("general")]
#[checks(DJ)] #[checks(DJ)]
async fn pause(ctx: &Context, msg: &Message) -> CommandResult { async fn pause(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Pausing playback for guild {}", guild.id); log::debug!("Pausing playback for guild {}", guild.id);
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { let queue = forward_error!(
player ctx,
} else { msg.channel_id,
return create_no_voicechannel_message(&ctx.http, msg.channel_id) get_queue_for_guild(ctx, &guild.id).await
.await );
.map_err(CommandError::from); let mut queue_lock = queue.lock().await;
};
let mut player = player.lock().await;
if let Some(_) = player.queue().current() {
player.toggle_paused().await?;
let is_paused = player.is_paused();
if is_paused { if let Some(_) = queue_lock.current() {
tracing::debug!("Paused"); queue_lock.pause();
if queue_lock.paused() {
log::debug!("Paused");
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("⏸️ Paused playback") m.content("⏸️ Paused playback")
}) })
.await?; .await?;
player.update_now_playing().await?; if let (Some(menu), Some((current, _))) =
(&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}
} else { } else {
tracing::debug!("Resumed"); log::debug!("Resumed");
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {
m.content("▶ Resumed playback") m.content("▶ Resumed playback")
}) })
.await?; .await?;
player.update_now_playing().await?; if let (Some(menu), Some((current, _))) =
(&queue_lock.now_playing_msg, queue_lock.current())
{
update_now_playing_msg(&ctx.http, menu, current.metadata(), true).await?;
}
} }
} else { } else {
msg.channel_id.say(ctx, "Nothing to pause").await?; msg.channel_id.say(ctx, "Nothing to pause").await?;

@ -1,16 +1,14 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult}; use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{ use crate::commands::music::{
get_channel_for_author, get_music_player_for_guild, get_songs_for_query, get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager,
join_channel, play_next_in_queue,
}; };
use crate::messages::music::now_playing::create_now_playing_msg;
use crate::providers::music::player::MusicPlayer;
use crate::providers::settings::{get_setting, Setting}; use crate::providers::settings::{get_setting, Setting};
use std::sync::Arc;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -22,49 +20,49 @@ use std::sync::Arc;
async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn play(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message(); let query = args.message();
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Play request received for guild {}", guild.id); log::debug!("Play request received for guild {}", guild.id);
let mut player = get_music_player_for_guild(ctx, guild.id).await; let manager = get_voice_manager(ctx).await;
let mut handler = manager.get(guild.id);
if player.is_none() { if handler.is_none() {
tracing::debug!("Not in a channel. Joining authors channel..."); log::debug!("Not in a channel. Joining authors channel...");
msg.guild(&ctx.cache).await.unwrap();
let channel_id = get_channel_for_author(&msg.author.id, &guild)?; let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; handler = Some(join_channel(ctx, channel_id, guild.id).await);
player = Some(music_player);
} }
let player = player.unwrap();
let handler_lock = forward_error!(
ctx,
msg.channel_id,
handler.ok_or(CommandError::from("I'm not in a voice channel"))
);
let songs = get_songs_for_query(&ctx, msg, query).await?; let songs = get_songs_for_query(&ctx, msg, query).await?;
let (play_first, create_now_playing) = { let queue = get_queue_for_guild(ctx, &guild.id).await?;
tracing::debug!("Adding song to queue");
let mut player_lock = player.lock().await; let play_first = {
log::debug!("Adding song to queue");
let mut queue_lock = queue.lock().await;
for song in songs { for song in songs {
player_lock.queue().add(song); queue_lock.add(song);
} }
let autoshuffle = get_setting(ctx, guild.id, Setting::MusicAutoShuffle) let autoshuffle = get_setting(ctx, guild.id, Setting::MusicAutoShuffle)
.await? .await?
.unwrap_or(false); .unwrap_or(false);
if autoshuffle { if autoshuffle {
tracing::debug!("Autoshuffeling"); log::debug!("Autoshuffeling");
player_lock.queue().shuffle(); queue_lock.shuffle();
} }
( queue_lock.current().is_none()
player_lock.queue().current().is_none(),
player_lock.now_playing_message().is_none(),
)
}; };
if play_first { if play_first {
tracing::debug!("Playing first song in queue"); log::debug!("Playing first song in queue");
let mut player_lock = player.lock().await; while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler_lock).await {}
player_lock.play_next().await?;
}
if create_now_playing {
let handle = create_now_playing_msg(ctx, Arc::clone(&player), msg.channel_id).await?;
let mut player_lock = player.lock().await;
player_lock.set_now_playing(handle).await;
} }
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;

@ -1,15 +1,13 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandResult}; use serenity::framework::standard::{Args, CommandError, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{ use crate::commands::music::{
get_channel_for_author, get_music_player_for_guild, get_songs_for_query, DJ_CHECK, get_channel_for_author, get_queue_for_guild, get_songs_for_query, get_voice_manager,
join_channel, play_next_in_queue, DJ_CHECK,
}; };
use crate::messages::music::now_playing::create_now_playing_msg;
use crate::providers::music::player::MusicPlayer;
use std::sync::Arc;
#[command] #[command]
#[only_in(guilds)] #[only_in(guilds)]
@ -22,43 +20,40 @@ use std::sync::Arc;
async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult { async fn play_next(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
let query = args.message(); let query = args.message();
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Playing song as next song for guild {}", guild.id); log::debug!("Playing song as next song for guild {}", guild.id);
let manager = get_voice_manager(ctx).await;
let mut handler = manager.get(guild.id);
let mut player = get_music_player_for_guild(ctx, guild.id).await; if handler.is_none() {
log::debug!("Not in a voice channel. Joining authors channel");
if player.is_none() { msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Not in a channel. Joining authors channel...");
let channel_id = get_channel_for_author(&msg.author.id, &guild)?; let channel_id = get_channel_for_author(&msg.author.id, &guild)?;
let music_player = MusicPlayer::join(ctx, guild.id, channel_id, msg.channel_id).await?; handler = Some(join_channel(ctx, channel_id, guild.id).await);
player = Some(music_player);
} }
let player = player.unwrap(); let handler = forward_error!(
ctx,
msg.channel_id,
handler.ok_or(CommandError::from("I'm not in a voice channel"))
);
let mut songs = get_songs_for_query(&ctx, msg, query).await?; let mut songs = get_songs_for_query(&ctx, msg, query).await?;
let (play_first, create_now_playing) = { let queue = get_queue_for_guild(ctx, &guild.id).await?;
let mut player_lock = player.lock().await; let play_first = {
let mut queue_lock = queue.lock().await;
songs.reverse(); songs.reverse();
tracing::debug!("Enqueueing songs as next songs in the queue"); log::debug!("Enqueueing songs as next songs in the queue");
for song in songs { for song in songs {
player_lock.queue().add_next(song); queue_lock.add_next(song);
} }
( queue_lock.current().is_none()
player_lock.queue().current().is_none(),
player_lock.now_playing_message().is_none(),
)
}; };
if play_first { if play_first {
let mut player_lock = player.lock().await; while !play_next_in_queue(&ctx.http, &msg.channel_id, &queue, &handler).await {}
player_lock.play_next().await?;
}
if create_now_playing {
let handle = create_now_playing_msg(ctx, Arc::clone(&player), msg.channel_id).await?;
let mut player_lock = player.lock().await;
player_lock.set_now_playing(handle).await;
} }
handle_autodelete(ctx, msg).await?; handle_autodelete(ctx, msg).await?;

@ -12,8 +12,8 @@ use crate::utils::context_data::get_database_from_context;
#[usage("")] #[usage("")]
#[bucket("general")] #[bucket("general")]
async fn playlists(ctx: &Context, msg: &Message) -> CommandResult { async fn playlists(ctx: &Context, msg: &Message) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Displaying playlists for guild {}", guild.id); log::debug!("Displaying playlists for guild {}", guild.id);
let database = get_database_from_context(ctx).await; let database = get_database_from_context(ctx).await;
let playlists = database.get_guild_playlists(guild.id.0).await?; let playlists = database.get_guild_playlists(guild.id.0).await?;

@ -1,11 +1,10 @@
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::get_music_player_for_guild; use crate::commands::music::get_queue_for_guild;
use crate::messages::music::no_voicechannel::create_no_voicechannel_message;
use crate::messages::music::queue::create_queue_menu; use crate::messages::music::queue::create_queue_menu;
use crate::providers::music::queue::Song; use crate::providers::music::queue::Song;
@ -16,24 +15,21 @@ use crate::providers::music::queue::Song;
#[aliases("q")] #[aliases("q")]
#[bucket("general")] #[bucket("general")]
async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::trace!("Displaying queue for guild {}", guild.id); log::trace!("Displaying queue for guild {}", guild.id);
let query = args let query = args
.iter::<String>() .iter::<String>()
.map(|s| s.unwrap().to_lowercase()) .map(|s| s.unwrap().to_lowercase())
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await { let queue = forward_error!(
player ctx,
} else { msg.channel_id,
return create_no_voicechannel_message(&ctx.http, msg.channel_id) get_queue_for_guild(ctx, &guild.id).await
.await );
.map_err(CommandError::from); let queue_lock = queue.lock().await;
}; let songs: Vec<(usize, Song)> = queue_lock
let mut player = player.lock().await;
let songs: Vec<(usize, Song)> = player
.queue()
.entries() .entries()
.into_iter() .into_iter()
.enumerate() .enumerate()
@ -53,7 +49,7 @@ async fn queue(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
}) })
.map(|(i, s)| (i, s.clone())) .map(|(i, s)| (i, s.clone()))
.collect(); .collect();
tracing::trace!("Songs are {:?}", songs); log::trace!("Songs are {:?}", songs);
if songs.len() == 0 { if songs.len() == 0 {
msg.channel_id msg.channel_id

@ -1,12 +1,11 @@
use crate::commands::common::handle_autodelete; use crate::commands::common::handle_autodelete;
use crate::commands::music::{get_music_player_for_guild, DJ_CHECK}; use crate::commands::music::{get_queue_for_guild, DJ_CHECK};
use crate::messages::music::no_voicechannel::create_no_voicechannel_message; use bot_serenityutils::core::SHORT_TIMEOUT;
use bot_serenityutils::ephemeral_message::EphemeralMessage;
use serenity::client::Context; use serenity::client::Context;
use serenity::framework::standard::macros::command; use serenity::framework::standard::macros::command;
use serenity::framework::standard::{Args, CommandError, CommandResult}; use serenity::framework::standard::{Args, CommandResult};
use serenity::model::channel::Message; use serenity::model::channel::Message;
use serenity_additions::core::SHORT_TIMEOUT;
use serenity_additions::ephemeral_message::EphemeralMessage;
#[command] #[command]
#[description("Removes a song from the queue")] #[description("Removes a song from the queue")]
@ -18,21 +17,19 @@ use serenity_additions::ephemeral_message::EphemeralMessage;
#[aliases("rms", "removesong", "remove-song")] #[aliases("rms", "removesong", "remove-song")]
#[checks(DJ)] #[checks(DJ)]
async fn remove_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { async fn remove_song(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let guild = msg.guild(&ctx.cache).unwrap(); let guild = msg.guild(&ctx.cache).await.unwrap();
tracing::debug!("Moving song for guild {}", guild.id); log::debug!("Moving song for guild {}", guild.id);
let pos = args.single::<usize>()?; let pos = args.single::<usize>()?;
let player = if let Some(player) = get_music_player_for_guild(ctx, guild.id).await {
player
} else {
return create_no_voicechannel_message(&ctx.http, msg.channel_id)
.await
.map_err(CommandError::from);
};
{ {
let mut player = player.lock().await; let queue = forward_error!(
player.queue().remove(pos); ctx,
msg.channel_id,
get_queue_for_guild(ctx, &guild.id).await
);
let mut queue_lock = queue.lock().await;
queue_lock.remove(pos);
} }
EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| { EphemeralMessage::create(&ctx.http, msg.channel_id, SHORT_TIMEOUT, |m| {

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save